Intermediate

Testing code that depends on the current date and time is notoriously painful. Functions that call datetime.now() return different values every second, which makes assertions fragile. Code that checks “is this subscription expired?” or “has this token timed out?” cannot be tested reliably without controlling what time the code thinks it is. Most developers resort to awkward monkeypatching, injecting mock datetime objects, or skipping time-sensitive tests altogether — all of which leave production bugs hiding in the gaps.

freezegun solves this problem cleanly. It intercepts calls to Python’s datetime, date, time, and time.time and returns a frozen or controlled time value that you specify. It works as a decorator, a context manager, or a standalone object — and it patches all modules that import datetime at test time, not just the one you are targeting. Install it with pip install freezegun. No other dependencies required.

This tutorial covers everything you need: freezing time with freeze_time as a decorator and context manager, moving time forward in increments with tick, testing expiry logic and time-zone-aware code, and integrating freezegun with pytest fixtures for clean test organization. By the end, you will be able to test any time-dependent code with complete determinism.

freezegun Quick Example

Here is the core pattern: wrap a test with @freeze_time and every call to datetime.now() inside that test returns the frozen date you specified.

# test_freezegun_quick.py
from freezegun import freeze_time
from datetime import datetime

def get_greeting():
    """Returns a greeting based on the current hour."""
    hour = datetime.now().hour
    if hour < 12:
        return "Good morning"
    elif hour < 18:
        return "Good afternoon"
    else:
        return "Good evening"

@freeze_time("2025-05-15 09:30:00")  # 9:30 AM
def test_morning_greeting():
    assert get_greeting() == "Good morning"
    print(f"Time during test: {datetime.now()}")

@freeze_time("2025-05-15 14:00:00")  # 2:00 PM
def test_afternoon_greeting():
    assert get_greeting() == "Good afternoon"
    print(f"Time during test: {datetime.now()}")

test_morning_greeting()
test_afternoon_greeting()
print(f"Time after tests: {datetime.now().strftime('%H:%M:%S')} (back to real time)")

Output:

Time during test: 2025-05-15 09:30:00
Time during test: 2025-05-15 14:00:00
Time after tests: 14:22:07 (back to real time)

The frozen time is scoped to the decorated function -- as soon as test_morning_greeting returns, datetime.now() reverts to real system time. No cleanup needed. The get_greeting() function is unaware it is being tested; it just calls datetime.now() normally, which is the whole point.

datetime.now() in production. freeze_time() in tests. Your future self thanks you.
datetime.now() in production. freeze_time() in tests. Your future self thanks you.

What Is freezegun and How Does It Work?

Freezegun works by patching the datetime module at the import level. When you activate freeze_time, it replaces the real datetime.datetime, datetime.date, datetime.datetime.now, and time.time with fake versions that return your specified time. Crucially, it patches all modules that have already imported datetime -- not just the module where you apply the decorator.

ApproachHow it worksLimitation
freezegunPatches datetime globallyDoes not patch C extensions that bypass datetime
unittest.mock.patchPatches one module at a timeMust patch every import location manually
Dependency injectionPass datetime as parameterRequires changing production code signatures
time.sleep mockingMocks sleep but not now()Does not help with current-time reads

The key advantage of freezegun over unittest.mock.patch is that you do not need to know which module imported datetime. If your service layer imports from datetime import datetime and your model layer imports import datetime, freezegun patches both automatically.

Using freeze_time as a Decorator and Context Manager

You can use freeze_time in three ways: as a class/function decorator, as a context manager inside a test, or as a manually started/stopped object. Each has its place.

# test_freezegun_modes.py
from freezegun import freeze_time
from datetime import datetime, date

# --- MODE 1: Decorator on a function ---
@freeze_time("2025-01-01")
def test_new_year():
    assert datetime.now().year == 2025
    assert date.today() == date(2025, 1, 1)
    print(f"Decorator mode: {datetime.now()}")

test_new_year()

# --- MODE 2: Context manager (freeze only part of a test) ---
def test_partial_freeze():
    real_before = datetime.now().year
    with freeze_time("2030-07-04 12:00:00"):
        frozen = datetime.now()
        assert frozen.year == 2030
        assert frozen.month == 7
    real_after = datetime.now().year
    assert real_before == real_after  # time unfrozen after 'with' block
    print(f"Context manager: frozen={frozen}, before/after real={real_before}")

test_partial_freeze()

# --- MODE 3: Manual start/stop (useful for class-level setUp/tearDown) ---
def test_manual():
    freezer = freeze_time("2024-06-15 08:00:00")
    freezer.start()
    print(f"Manual start: {datetime.now()}")
    freezer.stop()
    print(f"Manual stop: back to real time (year {datetime.now().year})")

test_manual()

Output:

Decorator mode: 2025-01-01 00:00:00
Context manager: frozen=2030-07-04 12:00:00, before/after real=2025
Manual start: 2024-06-15 08:00:00
Manual stop: back to real time (year 2025)

Use the decorator when the entire test function needs frozen time. Use the context manager when you need to test that code behaves differently before and after a specific moment -- for example, that a discount code is valid on day 1 but expired on day 8. Use manual start/stop in unittest.TestCase classes where you need to freeze time in setUp and restore it in tearDown.

Moving Time Forward with tick=True

By default, frozen time does not advance -- every call to datetime.now() returns the same value. Pass tick=True to start time at the frozen point but let it advance at real-world speed. This is useful when testing code that measures elapsed time with datetime.now().

# test_freezegun_tick.py
from freezegun import freeze_time
from datetime import datetime
import time

def measure_operation():
    """Returns elapsed time in seconds for an operation."""
    start = datetime.now()
    time.sleep(0.1)  # simulate 100ms work
    end = datetime.now()
    return (end - start).total_seconds()

# Without tick: start==end because time is frozen
@freeze_time("2025-03-01 10:00:00")
def test_no_tick():
    elapsed = measure_operation()
    print(f"No tick elapsed: {elapsed}s")  # 0.0 -- time never moves

# With tick=True: time advances from the frozen start point
@freeze_time("2025-03-01 10:00:00", tick=True)
def test_with_tick():
    start_now = datetime.now()
    elapsed = measure_operation()
    end_now = datetime.now()
    print(f"Tick elapsed: {elapsed:.3f}s (started at {start_now})")
    print(f"End time: {end_now}")

test_no_tick()
test_with_tick()

Output:

No tick elapsed: 0.0s
Tick elapsed: 0.101s (started at 2025-03-01 10:00:00)
End time: 2025-03-01 10:00:00.101232

With tick=True, time starts from 2025-03-01 10:00:00 and advances at the real wall-clock rate. This is the right mode when you want a deterministic start time but still need elapsed-time measurements to reflect reality -- for example, testing that a timeout fires after 30 seconds without actually waiting 30 seconds (use tick=True and advance time manually with freezer.move_to() instead).

tick=True: deterministic start, real elapsed. For when 0.0s lies.
tick=True: deterministic start, real elapsed. For when 0.0s lies.

Testing Expiry and Scheduling Logic

The most valuable use of freezegun is testing business logic that depends on dates -- subscription expiry, token timeouts, scheduled jobs that only run on certain days. These tests are impossible to write reliably without time control.

# test_expiry_logic.py
from freezegun import freeze_time
from datetime import datetime, timedelta

class Subscription:
    def __init__(self, start_date: datetime, duration_days: int):
        self.start_date = start_date
        self.expiry_date = start_date + timedelta(days=duration_days)

    def is_active(self) -> bool:
        return datetime.now() < self.expiry_date

    def days_remaining(self) -> int:
        delta = self.expiry_date - datetime.now()
        return max(0, delta.days)

    def renew(self, extra_days: int):
        self.expiry_date += timedelta(days=extra_days)

# --- Tests ---

@freeze_time("2025-06-01 00:00:00")
def test_subscription_active():
    sub = Subscription(datetime(2025, 5, 1), duration_days=90)
    assert sub.is_active() is True
    assert sub.days_remaining() == 59
    print(f"Active test: {sub.days_remaining()} days remaining")

@freeze_time("2025-08-05 00:00:00")  # after 90 days
def test_subscription_expired():
    sub = Subscription(datetime(2025, 5, 1), duration_days=90)
    assert sub.is_active() is False
    assert sub.days_remaining() == 0
    print("Expired test: subscription correctly expired")

@freeze_time("2025-07-25 00:00:00")  # 5 days before expiry
def test_subscription_renewal():
    sub = Subscription(datetime(2025, 5, 1), duration_days=90)
    assert sub.days_remaining() == 4
    sub.renew(30)
    assert sub.days_remaining() == 34
    print(f"Renewal test: {sub.days_remaining()} days after renewal")

test_subscription_active()
test_subscription_expired()
test_subscription_renewal()

Output:

Active test: 59 days remaining
Expired test: subscription correctly expired
Renewal test: 34 days after renewal

Notice that Subscription.__init__ takes an explicit start_date parameter rather than calling datetime.now() internally -- this is good design that makes the class easier to test. The is_active() and days_remaining() methods call datetime.now() to get the current time, which freezegun intercepts. Each test is completely deterministic regardless of when you run it.

Integrating freezegun with pytest Fixtures

For project-wide time control in a pytest suite, define a reusable fixture instead of applying @freeze_time to every test individually.

# conftest.py (put this in your project root)
import pytest
from freezegun import freeze_time

@pytest.fixture
def frozen_time():
    """Freeze time at a known reference point for all tests that need it."""
    freezer = freeze_time("2025-09-01 09:00:00")
    frozen = freezer.start()
    yield frozen
    freezer.stop()

@pytest.fixture
def future_time():
    """Fast-forward to 1 year in the future."""
    freezer = freeze_time("2026-09-01 09:00:00")
    frozen = freezer.start()
    yield frozen
    freezer.stop()
# test_with_fixtures.py
from datetime import datetime

def test_uses_frozen_time(frozen_time):
    now = datetime.now()
    assert now.year == 2025
    assert now.month == 9
    print(f"Fixture frozen: {now}")

def test_uses_future_time(future_time):
    now = datetime.now()
    assert now.year == 2026
    print(f"Future fixture: {now}")

Output (when run with pytest):

PASSED test_uses_frozen_time -- Fixture frozen: 2025-09-01 09:00:00
PASSED test_uses_future_time -- Future fixture: 2026-09-01 09:00:00

The yield pattern in the fixture ensures freezer.stop() always runs even if the test raises an exception -- equivalent to a finally block. This is the safest pattern for freezegun in pytest, as it avoids leaving time frozen if a test fails unexpectedly.

Real-Life Example: Testing a Token Expiry System

Token TTL: make it work in a test without time.sleep(3600).
Token TTL: make it work in a test without time.sleep(3600).
# test_token_system.py
from freezegun import freeze_time
from datetime import datetime, timedelta
import hashlib, secrets

class AuthToken:
    TTL_SECONDS = 3600  # 1 hour

    def __init__(self, user_id: str):
        self.user_id = user_id
        self.token = secrets.token_hex(16)
        self.created_at = datetime.now()
        self.expires_at = self.created_at + timedelta(seconds=self.TTL_SECONDS)

    def is_valid(self) -> bool:
        return datetime.now() < self.expires_at

    def time_remaining(self) -> int:
        """Returns seconds remaining, or 0 if expired."""
        delta = self.expires_at - datetime.now()
        return max(0, int(delta.total_seconds()))

    def refresh(self):
        """Extend token lifetime from now."""
        self.expires_at = datetime.now() + timedelta(seconds=self.TTL_SECONDS)

# Test: token is valid immediately after creation
@freeze_time("2025-10-01 12:00:00")
def test_token_valid_at_creation():
    token = AuthToken("user123")
    assert token.is_valid() is True
    assert token.time_remaining() == 3600
    print(f"Created: {token.created_at}, expires: {token.expires_at}")

# Test: token is expired 1 hour and 1 second later
def test_token_expires():
    with freeze_time("2025-10-01 12:00:00") as frozen:
        token = AuthToken("user123")
        assert token.is_valid() is True

    with freeze_time("2025-10-01 13:00:01"):
        assert token.is_valid() is False
        assert token.time_remaining() == 0
    print("Expiry test: token correctly expired after TTL")

# Test: refreshing extends lifetime
def test_token_refresh():
    with freeze_time("2025-10-01 12:00:00"):
        token = AuthToken("user123")

    with freeze_time("2025-10-01 12:59:00"):  # 1 min before expiry
        assert token.time_remaining() == 60
        token.refresh()

    with freeze_time("2025-10-01 13:58:00"):  # 1h after refresh
        assert token.is_valid() is True
        print(f"Refreshed: {token.time_remaining()}s remaining")

test_token_valid_at_creation()
test_token_expires()
test_token_refresh()

Output:

Created: 2025-10-01 12:00:00, expires: 2025-10-01 13:00:00
Expiry test: token correctly expired after TTL
Refreshed: 120s remaining

The test_token_refresh test uses three separate freeze_time context managers to simulate creating a token, approaching expiry, refreshing it, and confirming the new expiry -- all without time.sleep(). This test runs in milliseconds and is completely deterministic.

Frequently Asked Questions

Does freezegun patch all modules that import datetime?

Yes -- this is freezegun's key feature over unittest.mock.patch. When you activate freeze_time, it patches datetime.datetime, datetime.date, datetime.datetime.now, datetime.datetime.utcnow, datetime.date.today, and time.time across all loaded modules. However, it does not patch C extensions that call the system clock directly (e.g., some database drivers or asyncio event loops). In those cases you may still need additional mocking.

Does freezegun work with timezone-aware datetimes?

Yes. Pass a timezone-aware string like freeze_time("2025-06-01 12:00:00+10:00") or use freeze_time(datetime(2025, 6, 1, 12, tzinfo=timezone.utc)). Freezegun will return timezone-aware datetimes from datetime.now(tz) when called with a timezone argument. Without a timezone argument, datetime.now() returns naive datetimes at the frozen local time, same as the real function.

Does freezegun work with async code?

Yes -- freezegun patches the underlying datetime module which is called by both synchronous and asynchronous code. However, freezegun does not mock asyncio.sleep or event loop time. If your async code relies on the event loop clock (common in timeout implementations using asyncio.wait_for), you will need pytest-anyio or time_machine (a faster alternative to freezegun for async scenarios) to control event loop time.

Can I freeze to dates in the past or far future?

Yes -- any valid date string or datetime object works. freeze_time("1985-10-26 01:21:00") is perfectly valid. Dates before the Unix epoch (1970-01-01) also work. One practical caveat: some systems and libraries behave unexpectedly with dates far outside their intended range -- for example, SSL certificate validation or JWT expiry checks that use hard-coded year limits.

When should I use time-machine instead of freezegun?

time-machine is a newer alternative that works at the C level, making it faster and more reliable for async code and C extensions. If you are starting a new project with Python 3.8+, time-machine is worth considering. But if you already use freezegun and it covers your needs, there is no pressing reason to migrate -- freezegun is stable, widely used, and covers 95% of time-mocking scenarios.

Conclusion

Freezegun turns time-dependent tests from flaky guesswork into deterministic, maintainable specs. You have learned the three usage modes -- decorator, context manager, and manual start/stop -- plus tick=True for advancing time at real speed, and the pytest fixture pattern for reusable time control across a test suite. The real-life token expiry example demonstrated how to test multi-step time progressions without any time.sleep() calls.

Extend the token system by testing edge cases: a token created one second before midnight, a refresh that should fail because the token is already expired, or a batch of tokens created at different times and checked simultaneously. Freezegun makes all of these easy to write and fast to run.

For the full API reference and advanced configuration options, see the freezegun GitHub repository. For async-heavy projects, also look at time-machine as a comparison.