Freezegun replaces common date and time sources during a test. It can verify expiration, deadlines, and schedules without relying on the real clock or adding sleep() to the suite.
The problem is not only speed. A test tied to the machine clock can fail at midnight, during a civil-time transition, or when two assertions cross a second boundary. Freezing an instant turns that external dependency into reproducible input, so a failure can be repeated locally and in CI.
Freeze an explicit instant
from datetime import datetime, timezone
from freezegun import freeze_time
def expired(deadline: datetime) -> bool:
return datetime.now(timezone.utc) >= deadline
@freeze_time("2026-08-20 18:00:00+00:00")
def test_expiration() -> None:
deadline = datetime(2026, 8, 20, 17, 59, tzinfo=timezone.utc)
assert expired(deadline)
Use an explicit offset or UTC so the test does not change with the machine timezone. Cover the instant immediately before, exactly at, and immediately after a deadline. Those boundaries reveal more defects than many arbitrary timestamps.
Decorators, contexts, and fixtures
The decorator works well when the entire test shares one instant. A context manager restricts the change to the code that needs it:
from datetime import date
from freezegun import freeze_time
def day_label() -> str:
return date.today().isoformat()
def test_day_label() -> None:
with freeze_time("2026-12-31"):
assert day_label() == "2026-12-31"
# Outside the block, date.today() behaves normally again.
A pytest fixture can centralize a reference instant:
import pytest
from freezegun import freeze_time
@pytest.fixture
def frozen_clock():
with freeze_time("2026-08-20 18:00:00+00:00") as clock:
yield clock
Avoid making such a fixture autouse for the entire suite. That hides the dependency and may affect libraries that measure timeouts, cache lifetimes, or durations. A descriptive fixture name makes the temporary global effect visible.
Advance time without waiting
Use tick() to test progressive expiration. It keeps the test fast and states exactly how much time passes:
from datetime import datetime, timedelta, timezone
from freezegun import freeze_time
def is_valid(created_at: datetime, ttl: timedelta) -> bool:
return datetime.now(timezone.utc) < created_at + ttl
def test_token_expires_after_five_minutes() -> None:
with freeze_time("2026-08-20 18:00:00+00:00") as clock:
created_at = datetime.now(timezone.utc)
ttl = timedelta(minutes=5)
clock.tick(delta=timedelta(minutes=4, seconds=59))
assert is_valid(created_at, ttl)
clock.tick(delta=timedelta(seconds=1))
assert not is_valid(created_at, ttl)
auto_tick_seconds advances the clock on each intercepted read. Use it only when the number and order of reads are part of the scenario. Otherwise, an extra datetime.now() added during refactoring can change the result without changing the business rule. Manual advancement is usually clearer.
Timezones, naive values, and daylight saving time
Freezegun controls the instant, but it does not repair an ambiguous time model. Calling datetime.now() without a timezone still creates a naive value. For events that represent global instants, prefer timezone-aware values and normalize storage and comparisons to UTC. Convert to a user's timezone only at presentation boundaries.
tz_offset can simulate a difference from UTC, but it is not a replacement for an IANA zone with historical rules. If behavior depends on a daylight-saving transition, freeze a UTC instant, convert it with ZoneInfo, and test that conversion explicitly. Include repeated or nonexistent local times when the domain can encounter them.
Import and process boundaries
The common from datetime import datetime pattern is supported, but native extensions, separate processes, and remote services may consult another clock. A worker started in a different process does not automatically inherit the test process patch. At those boundaries, pass the instant in a message, configure the worker separately, or inject a clock abstraction.
Use Freezegun's ignore option only for modules that must see real time, and document why. A broad exclusion list can hide an integration that remains dependent on the machine clock.
When to inject a clock
Core domain logic is often easier to test when time is an explicit dependency:
from collections.abc import Callable
from datetime import datetime, timezone
Clock = Callable[[], datetime]
def can_renew(expires_at: datetime, now: Clock) -> bool:
remaining = expires_at - now()
return remaining.total_seconds() <= 3600
def utc_now() -> datetime:
return datetime.now(timezone.utc)
Pass utc_now in production and a fixed function in tests. This avoids a global patch and exposes the dependency in the function's contract. Freezegun remains valuable for legacy code, frameworks, and integration tests where changing every signature is undesirable. Both approaches can coexist: inject a clock into the core and freeze time at the edges.
Monotonic clocks and concurrency
Wall time answers "what time is it?" A monotonic clock measures elapsed time and does not move backward when the system clock is corrected. Timeouts, retry delays, and duration metrics should generally use time.monotonic() or an equivalent abstraction.
Current Freezegun versions affect monotonic APIs in documented scenarios, but that does not make civil time and elapsed duration the same concept. Check the installed version and write a focused test when an async framework, event loop, or HTTP client relies on those APIs. Avoid a process-wide freeze while unrelated tests run in threads, because they may observe the patched clock.
A practical test matrix
- immediately before, at, and after a deadline;
- day, month, and year boundaries;
- leap years when February affects the rule;
- aware and naive inputs, including expected rejection;
- conversion from UTC to the displayed timezone;
- TTL advancement, renewal, and grace windows;
- timestamp serialization and restoration;
- external processes or services outside the freeze.
Assert the application's observable rule, not Freezegun internals such as the exact patched class. This keeps tests resilient across library updates.
Installation and maintenance
Pin an appropriate version range according to the project's dependency policy and upgrade deliberately. During an upgrade, run time-sensitive tests first, especially asynchronous integrations and packages containing native code. If users submit dates, test parsing separately: freezing the clock does not validate formats, offsets, or calendar rules.
Designing maintainable scenarios
Choose an instant that explains the rule. A random far-future date makes intent harder to see, while always using January 1 can hide month-length and leap-year defects. Name business windows such as RENEWAL_WINDOW, and explain any grace period in the test rather than embedding unexplained numbers.
Keep one behavioral reason per test. A single frozen context that checks expiration, formatting, scheduling, and persistence is difficult to diagnose. Separate those concerns while reusing a small clock fixture where appropriate. Conversely, do not test every minute of a year when equivalence classes and boundaries cover the same rule.
If application code caches a value at module import time, freezing after the import may be too late. That is usually a design signal: compute the value when needed or make the dependency explicit. Reloading modules in tests can work, but it changes global state and often creates order-dependent failures.
Always clean up patches through the decorator or context manager, even when an assertion raises. Avoid starting a freeze manually without a matching stop in a finally block. Leaked time state can make the next test fail in a seemingly unrelated module.
Finally, remove sleep() calls that exist only to coordinate assertions. Real sleeping may still belong in an end-to-end test of an external scheduler, but that slower test should be isolated and should not replace deterministic unit coverage.
Freezegun supports ticking and manual movement, but wall time and monotonic time serve different purposes. Internal duration measurement should normally use a monotonic source. Test the actual library behavior and do not assume every time API is intercepted.
For core business logic, injecting a now() callable can make the dependency explicit and reduce test coupling to implementation. Freezegun is particularly useful at integration boundaries where code calls datetime.now() or date.today() directly.
The zoneinfo and timezones guide helps avoid naive datetimes. The official Freezegun repository, accessed July 22, 2026, documents decorators, context managers, ticking, timezone offsets, and supported APIs.