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.

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.
| Approach | How it works | Limitation |
|---|---|---|
| freezegun | Patches datetime globally | Does not patch C extensions that bypass datetime |
| unittest.mock.patch | Patches one module at a time | Must patch every import location manually |
| Dependency injection | Pass datetime as parameter | Requires changing production code signatures |
| time.sleep mocking | Mocks 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).

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

# 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.