Skip to content

Fix: freezegun Not Working — Datetime Not Frozen, Timezone Issues, and Async Tests

FixDevs ·

Quick Answer

How to fix freezegun errors — freeze_time decorator not affecting datetime.now, timezone-aware datetime mismatch, time.time not frozen, async test time leak, third-party library still using real time, and tick parameter behavior.

The Error

You decorate a test with @freeze_time and datetime.now() returns the real time:

from freezegun import freeze_time
from datetime import datetime

@freeze_time("2025-01-01")
def test_frozen():
    print(datetime.now())   # Today's real date, not 2025-01-01

Or timezone-aware datetimes don’t match:

from datetime import datetime, timezone

@freeze_time("2025-01-01 12:00:00")
def test():
    assert datetime.now(timezone.utc) == datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
    # AssertionError — they're different

Or async tests leak time into other tests:

@pytest.mark.asyncio
@freeze_time("2025-01-01")
async def test_async():
    await asyncio.sleep(1)   # Real sleep — time advances
    assert datetime.now() == datetime(2025, 1, 1)   # Fails — datetime advanced

Or a third-party library inside the frozen scope sees real time:

@freeze_time("2025-01-01")
def test_jwt():
    token = jwt.encode({"exp": 1234567890}, "secret", algorithm="HS256")
    # JWT library uses real time for expiry, ignores freeze_time

Or auto_tick_seconds doesn’t behave as expected:

@freeze_time("2025-01-01", auto_tick_seconds=1)
def test():
    t1 = time.time()
    t2 = time.time()
    # t2 == t1, not t1 + 1 — wrong behavior

freezegun is the dominant time-mocking library for Python tests — @freeze_time("2025-01-01") patches datetime, time, and related modules to return a fixed moment. The decorator-based API is simple, but the interaction with timezone-aware datetimes, async code, and libraries that cache time produces subtle failures. This guide covers each common issue.

Why This Happens

freezegun patches the standard library modules (datetime, time, calendar) at the Python level by replacing their functions with mocks. Code that imported these modules before freezegun activates uses the unpatched versions — common with from datetime import datetime as DT style imports cached at module load time.

Async code runs in event loops where time is tracked separately by asyncioawait asyncio.sleep(N) actually waits N seconds of real time, regardless of freezegun. Real sleeps advance the wall clock even while datetime is frozen.

Fix 1: Decorator and Context Manager Patterns

from freezegun import freeze_time
from datetime import datetime
import time

# Decorator
@freeze_time("2025-01-01 12:00:00")
def test_with_decorator():
    assert datetime.now() == datetime(2025, 1, 1, 12, 0, 0)
    assert int(time.time()) == 1735732800

# Context manager
def test_with_context():
    with freeze_time("2025-01-01 12:00:00"):
        assert datetime.now() == datetime(2025, 1, 1, 12, 0, 0)
    # Outside the context — real time again

# Class decorator (all methods)
@freeze_time("2025-01-01")
class TestFrozenSuite:
    def test_one(self):
        assert datetime.now().year == 2025
    def test_two(self):
        assert datetime.now().year == 2025

Time format flexibility:

freeze_time("2025-01-01")                          # Date only — midnight
freeze_time("2025-01-01 12:00:00")                 # ISO datetime
freeze_time("2025-01-01T12:00:00")                 # ISO with T separator
freeze_time("2025-01-01 12:00:00+00:00")           # With timezone
freeze_time(datetime(2025, 1, 1, 12, 0, 0))        # datetime object
freeze_time(date(2025, 1, 1))                       # date object

Common Mistake: Importing datetime at module level before freezegun is configured. The cached reference may point at the real datetime even inside @freeze_time. Always import inside the function, or use freezegun.api.register_call_stack_inspection to handle the edge case. In practice, from datetime import datetime then using datetime.now() inside the test works fine — the issue is only when libraries cache the datetime class itself.

Fix 2: Timezone-Aware Datetimes

from freezegun import freeze_time
from datetime import datetime, timezone

@freeze_time("2025-01-01 12:00:00")
def test():
    # freeze_time defaults to UTC
    naive = datetime.now()
    aware = datetime.now(timezone.utc)

    print(naive)   # 2025-01-01 12:00:00 (no tz)
    print(aware)   # 2025-01-01 12:00:00+00:00

Specify timezone explicitly:

from datetime import timezone, timedelta
from freezegun import freeze_time

tz_jst = timezone(timedelta(hours=9))   # Japan Standard Time

@freeze_time("2025-01-01 12:00:00", tz_offset=9)
def test_tokyo_time():
    # freeze_time interprets the string as UTC, but datetime.now(tz_jst) shows local
    local = datetime.now(tz_jst)
    assert local == datetime(2025, 1, 1, 21, 0, 0, tzinfo=tz_jst)
    # UTC 12:00 = JST 21:00

Or freeze with explicit tz datetime:

from datetime import datetime, timezone

@freeze_time(datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc))
def test():
    assert datetime.now(timezone.utc).hour == 12

Common Mistake: Comparing a naive datetime.now() to an aware datetime constant. The naive one returns “current frozen time, no tz” while the aware one returns “current frozen time in UTC”. They’re equal numerically but Python raises TypeError: can't compare offset-naive and offset-aware datetimes. Always be consistent — either both aware or both naive.

# WRONG
@freeze_time("2025-01-01")
def test():
    assert datetime.now() == datetime(2025, 1, 1, tzinfo=timezone.utc)
    # TypeError

# CORRECT
@freeze_time("2025-01-01")
def test():
    assert datetime.now(timezone.utc) == datetime(2025, 1, 1, tzinfo=timezone.utc)

Fix 3: Ticking and Time Advancement

By default, freezegun returns the same instant every time you call time.time() or datetime.now(). To advance time:

from freezegun import freeze_time
from datetime import datetime, timedelta

# Manual advance
with freeze_time("2025-01-01") as frozen:
    assert datetime.now() == datetime(2025, 1, 1, 0, 0, 0)

    frozen.tick(delta=timedelta(seconds=30))
    assert datetime.now() == datetime(2025, 1, 1, 0, 0, 30)

    frozen.tick(delta=timedelta(minutes=5))
    assert datetime.now() == datetime(2025, 1, 1, 0, 5, 30)

    # Or move to a specific time
    frozen.move_to("2025-01-02 09:00:00")
    assert datetime.now() == datetime(2025, 1, 2, 9, 0, 0)

Auto-ticking — time advances by N seconds on each call:

@freeze_time("2025-01-01", auto_tick_seconds=1)
def test():
    t1 = time.time()
    t2 = time.time()
    assert t2 - t1 == 1   # Each call advances 1 second

auto_tick_seconds=1 is useful for testing rate limiting or timeout logic — every time check advances by a known interval.

tick=True (real time, just shifted):

@freeze_time("2025-01-01", tick=True)
def test():
    # Time progresses in real time, but starting from 2025-01-01
    t1 = time.time()
    time.sleep(0.1)
    t2 = time.time()
    assert t2 - t1 >= 0.1

Common Mistake: Using tick=True for tests that need deterministic timing. The wall clock advances during test execution — small delays compound and tests become flaky. Prefer auto_tick_seconds or explicit tick() calls.

Fix 4: Async Code and asyncio.sleep

import asyncio
from freezegun import freeze_time
from datetime import datetime

@freeze_time("2025-01-01")
async def test_async():
    start = datetime.now()
    await asyncio.sleep(1)   # Real 1-second wait
    end = datetime.now()
    print(end - start)   # 0:00:01 (real time advanced)

freezegun freezes datetime/time but asyncio.sleep uses the event loop’s monotonic clock, which freezegun doesn’t patch. Real sleeps happen and the datetime stays frozen — confusing mismatch.

For async tests, mock asyncio.sleep separately or use a time-skipping pattern:

from unittest.mock import patch

@pytest.mark.asyncio
@freeze_time("2025-01-01")
async def test_async():
    async def fake_sleep(seconds):
        # Advance freezegun's time when sleep is called
        with freeze_time(datetime.now() + timedelta(seconds=seconds)):
            return

    with patch("asyncio.sleep", new=fake_sleep):
        await my_async_function_that_sleeps()

Pro Tip: For testing scheduled async code, use libraries built for time-virtualized async testing — aiojobs, trio.testing.MockClock, or pytest-asyncio with mock_clock fixtures. freezegun alone doesn’t simulate asyncio’s internal scheduling.

Trio’s MockClock (for trio-based async code):

import trio
from trio.testing import MockClock

async def test_with_clock():
    async with trio.open_nursery() as nursery:
        clock = MockClock()
        async def task():
            await trio.sleep(60)   # MockClock makes this instant
            return "done"
        nursery.start_soon(task)

Fix 5: Libraries That Cache Time

Some libraries grab the current time at module load and cache it:

# my_module.py
from datetime import datetime
START_TIME = datetime.now()   # Captured once at import

# test_my_module.py
@freeze_time("2025-01-01")
def test():
    from my_module import START_TIME
    assert START_TIME == datetime(2025, 1, 1)   # Fails — captured before freeze

Solution — reload the module under freeze:

import importlib
from freezegun import freeze_time

@freeze_time("2025-01-01")
def test():
    import my_module
    importlib.reload(my_module)
    assert my_module.START_TIME == datetime(2025, 1, 1)

Or restructure the code to use lazy evaluation:

# my_module.py
from datetime import datetime

def get_start_time():
    return datetime.now()

# Always returns current time, freezable in tests

Common Mistake: JWT, OAuth, and signed-cookie libraries often have now() baked into their token generation. The frozen time doesn’t propagate if these libraries use time.time() cached at import. Inspect the library — most modern libraries (pyjwt, authlib) accept a current_time parameter, or you can pass the time explicitly:

import jwt
from freezegun import freeze_time
import time

@freeze_time("2025-01-01")
def test_jwt():
    token = jwt.encode(
        {"exp": int(time.time()) + 3600},   # Uses frozen time
        "secret",
        algorithm="HS256",
    )

Fix 6: pytest Fixtures with freeze_time

import pytest
from freezegun import freeze_time
from datetime import datetime

@pytest.fixture
def frozen_time():
    with freeze_time("2025-01-01") as frozen:
        yield frozen

def test_with_fixture(frozen_time):
    assert datetime.now() == datetime(2025, 1, 1)
    frozen_time.tick(timedelta(hours=1))
    assert datetime.now() == datetime(2025, 1, 1, 1, 0, 0)

Parametrize across multiple frozen times:

import pytest
from freezegun import freeze_time

@pytest.mark.parametrize("frozen_at", [
    "2025-01-01",
    "2025-06-15",
    "2025-12-31",
])
def test_multiple_dates(frozen_at):
    with freeze_time(frozen_at):
        # Test logic for each date
        ...

pytest-freezer (modern fork) — better integration:

pip install pytest-freezer
def test_with_marker(freezer):
    freezer.move_to("2025-01-01")
    assert datetime.now().year == 2025
    freezer.move_to("2025-12-31")
    assert datetime.now().month == 12

For pytest fixture patterns that work cleanly with freezegun, see pytest fixture not found.

Fix 7: Specific Use Cases

Test scheduled jobs:

from freezegun import freeze_time
from datetime import datetime, timedelta

def is_business_hour():
    now = datetime.now()
    return 9 <= now.hour < 17 and now.weekday() < 5

@freeze_time("2025-04-24 10:00:00")   # Thursday 10am
def test_in_hours():
    assert is_business_hour() is True

@freeze_time("2025-04-24 22:00:00")   # Thursday 10pm
def test_out_of_hours():
    assert is_business_hour() is False

@freeze_time("2025-04-26 10:00:00")   # Saturday 10am
def test_weekend():
    assert is_business_hour() is False

Test cache expiration:

@freeze_time("2025-01-01 12:00:00") as frozen
def test_cache_expires():
    cache.set("key", "value", ttl=300)   # 5 minutes
    assert cache.get("key") == "value"

    frozen.tick(timedelta(seconds=299))
    assert cache.get("key") == "value"

    frozen.tick(timedelta(seconds=2))
    assert cache.get("key") is None   # Expired

Test rate limiting:

@freeze_time("2025-01-01 12:00:00") as frozen
def test_rate_limit():
    for _ in range(5):
        assert rate_limiter.allow("user-1") is True

    assert rate_limiter.allow("user-1") is False   # 6th request blocked

    frozen.tick(timedelta(minutes=1))
    assert rate_limiter.allow("user-1") is True   # Window reset

Common Mistake: Testing time-sensitive code without freezing time. Tests that pass at 10:30am may fail at 11:00am because of a “current time” check inside the code. Always freeze time in any test that touches datetime.now(), time.time(), or anything that uses them.

Fix 8: Performance and Limitations

freezegun has overhead — patching the datetime module on entry/exit takes time. For tests that repeatedly enter and exit freeze_time contexts, the overhead adds up.

Use a wider scope when possible:

# SLOW — freezes per call
def test_many():
    for date in dates:
        with freeze_time(date):
            do_something()

# FASTER — freeze once, advance via tick
with freeze_time(dates[0]) as frozen:
    for date in dates:
        frozen.move_to(date)
        do_something()

Skip freezing entirely when datetime calls are rare:

# If only ONE datetime call matters, mock it directly
from unittest.mock import patch
from datetime import datetime

with patch("mymodule.datetime") as mock_dt:
    mock_dt.now.return_value = datetime(2025, 1, 1)
    do_something()

Direct mocking is faster than full freezegun for tightly-scoped patches.

freezegun doesn’t patch:

  • C extensions that read time directly (rare; some crypto libs)
  • os.times(), os.stat() timestamps
  • File system mtimes
  • The system kernel’s clock (gettimeofday syscall directly)

For these, use platform-specific mocking or restructure the code to inject time as a dependency.

Still Not Working?

freezegun vs Alternatives

  • freezegun — Most popular, mature, covers most Python time APIs.
  • pytest-freezer — Newer fork with better pytest integration.
  • time-machine — Faster (Cython implementation), drop-in replacement for freezegun.

For projects with thousands of frozen-time tests, time-machine can cut test suite time significantly. API is nearly identical.

Integration with Faker

When generating test data with Faker that includes timestamps, fix the seed AND freeze time for fully deterministic output:

from freezegun import freeze_time
from faker import Faker

fake = Faker()
fake.seed_instance(42)

@freeze_time("2025-01-01")
def test_deterministic_data():
    timestamp = fake.date_time_this_year()   # Predictable
    name = fake.name()                         # Predictable (from seed)

Combining with Hypothesis

For property-based tests with time, freeze inside the test rather than via decorator:

from hypothesis import given, strategies as st
from freezegun import freeze_time

@given(st.datetimes(min_value=datetime(2020, 1, 1), max_value=datetime(2030, 12, 31)))
def test_with_random_time(some_datetime):
    with freeze_time(some_datetime):
        result = my_function()
        assert result.created_at == some_datetime

For Hypothesis-specific patterns that combine with freezegun, see Hypothesis not working.

Async-Aware Time Mocking

For asyncio code that does scheduled work, pytest plugins like pytest-asyncio and frameworks like anyio provide MockClock-style helpers. Combine with freezegun for datetime calls:

import pytest
import asyncio
from freezegun import freeze_time

@pytest.mark.asyncio
@freeze_time("2025-01-01")
async def test_async_workflow(monkeypatch):
    # Mock asyncio.sleep to not actually wait
    async def instant_sleep(seconds):
        pass

    monkeypatch.setattr(asyncio, "sleep", instant_sleep)
    await my_workflow_with_delays()

For asyncio testing patterns that integrate with time mocking, see Python asyncio not running.

Debugging Why Time Isn’t Frozen

import time
import datetime
from freezegun import freeze_time

@freeze_time("2025-01-01")
def debug_time():
    print(f"time.time(): {time.time()}")           # 1735689600.0 if frozen
    print(f"datetime.now(): {datetime.datetime.now()}")
    print(f"datetime.utcnow(): {datetime.datetime.utcnow()}")

debug_time()

If the printed times match 2025-01-01, freezegun is active. If they show today’s date, something is bypassing freezegun — likely a cached import or a library using a non-patched time source.

For broader testing patterns that combine freezegun with mocking, see pytest fixture not found. For Moto-based AWS mocking that often pairs with freezegun for time-sensitive AWS workflows, see Moto not working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles