freezegun
FreezeGun lets a Python test pretend the clock says whatever you want. You wrap a test in @freeze_time("2012-01-14") or a with block, and every call to datetime.datetime.now(), datetime.date.today(), time.time(), time.localtime(), time.gmtime(), and time.strftime() returns that frozen moment instead of the real one. It does this by replacing the datetime and time classes and then walking modules that already imported them so their references point at the fakes too, which is why it works on code you did not write. Inside the block you can also step the clock forward by hand with tick(), jump to a different date with move_to(), or let it run at an automatic increment per call.
Still the default answer for time in Python tests, and the API is pleasant enough that you will write more time-sensitive tests than you otherwise would. Reach for time-machine instead if freeze overhead shows in your suite runtime, or if the year-long gap since the last release worries you more than switching cost.
Use it if
- You are testing anything that expires: sessions, JWTs, password reset tokens, cache TTLs, trial periods, rate-limit windows. Freezing the clock beats sprinkling sleep() calls or injecting a clock object through five layers of code
- The code under test calls datetime.now() directly and you cannot refactor it to take an injectable clock, which describes most real codebases and every third-party library you depend on
- You need a test to be deterministic across a date boundary: end-of-month billing, leap day, daylight saving transitions, or a report that behaves differently on the first of the month
- You want to simulate elapsed time inside one test without waiting: freeze, act, tick forward 30 days, assert the subscription lapsed
- Maintenance has gone quiet: version 1.5.5 shipped in August 2025 and the repository's last push was August 2025, with 120 open issues (161 counting PRs). It still works, but do not expect a fast fix for a Python 3.15 incompatibility
- Your test suite is large and time-sensitive: freezing works by scanning loaded modules for references to datetime and swapping them, so every enter and exit costs real time. Suites with thousands of freezes measurably slow down, which is exactly the problem time-machine was written to fix with a C extension
- The clock you actually care about is not Python's: PostgreSQL NOW(), Redis TTLs, S3 presign expiry, and anything inside a C extension that reads the system clock directly keep telling the truth while your Python code lives in 2012, which produces confusing half-frozen behavior
- You are freezing time inside async code without thinking it through: time.monotonic() is frozen too, so asyncio.sleep() and any timeout built on the event loop clock stall forever unless you pass real_asyncio=True
- Your project already has a clean seam for time (a now() function or a clock dependency you pass in). Injecting a fake clock is faster, more explicit, and does not patch the interpreter out from under unrelated code
Setup reality
pip install freezegun pulls one dependency, python-dateutil, and needs Python 3.8 or newer. No compiler, no config file, nothing to register with pytest. The annoyances show up later. Some libraries break when their datetime references get swapped, so freezegun ships a default ignore list covering threading, Queue, selenium, gi, parts of pytest internals, and a few others; when a library you use misbehaves under a freeze you add it yourself with freeze_time(ignore=[...]) or globally via freezegun.configure(extend_ignore_list=[...]). Default arguments evaluated at import time are never frozen, because they were computed before your test ran. Decorating a class freezes each callable on it, which the README itself hedges about with "may not work in every case". And if you use asyncio, remember that monotonic time is frozen unless you opt out with real_asyncio=True.
Patterns
Freeze the clock for a whole testfreeze-with-decorator
import datetime
from freezegun import freeze_time
@freeze_time("2012-01-14")
def test_expiry():
assert datetime.datetime.now() == datetime.datetime(2012, 1, 14)
assert datetime.date.today() == datetime.date(2012, 1, 14)A bare date string freezes at midnight. The decorator also works on a unittest.TestCase subclass (it covers setUp and tearDown too) and on async test coroutines.
Freeze only part of a testfreeze-with-context-manager
import datetime
from freezegun import freeze_time
def test_partial():
real_now = datetime.datetime.now()
with freeze_time("2012-01-14 12:00:01"):
assert datetime.datetime.now() == datetime.datetime(2012, 1, 14, 12, 0, 1)
assert datetime.datetime.now() >= real_nowUse this when you want most of the test on the real clock and only one call frozen. Everything is restored on exit, including on exceptions.
Start and stop the freeze by handmanual-start-stop
import datetime
from freezegun import freeze_time
freezer = freeze_time("2012-01-14 12:00:01")
freezer.start()
assert datetime.datetime.now() == datetime.datetime(2012, 1, 14, 12, 0, 1)
freezer.stop()Useful in setUp/tearDown pairs or session fixtures. If an exception escapes before stop(), the clock stays frozen for the rest of the process, so wrap it in try/finally outside of a fixture.
Advance the frozen clock inside a testtick-forward-manually
import datetime
from freezegun import freeze_time
def test_token_expires():
with freeze_time("2024-03-01 09:00:00") as frozen:
token = issue_token(ttl=datetime.timedelta(minutes=15))
frozen.tick() # default step is 1 second
assert token.is_valid()
frozen.tick(delta=datetime.timedelta(minutes=20))
assert not token.is_valid()tick() accepts a timedelta or a float number of seconds. time.monotonic() moves with it, so code measuring elapsed time sees the jump as well.
Jump to a completely different datemove-to-a-date
import datetime
from freezegun import freeze_time
@freeze_time("2012-01-14", as_arg=True)
def test_month_rollover(frozen):
assert datetime.date.today() == datetime.date(2012, 1, 14)
frozen.move_to("2014-02-12")
assert datetime.date.today() == datetime.date(2014, 2, 12)move_to takes anything freeze_time takes: a string, a date, or a datetime. as_arg=True passes the controller in as the first positional argument; as_kwarg='name' passes it by keyword instead.
Let time keep running from a fixed startticking-clock
import datetime
from freezegun import freeze_time
@freeze_time("2020-01-14", tick=True)
def test_time_still_moves():
assert datetime.datetime.now() > datetime.datetime(2020, 1, 14)
@freeze_time("2020-01-14", auto_tick_seconds=15)
def test_fixed_step():
first = datetime.datetime.now()
second = datetime.datetime.now()
assert second - first == datetime.timedelta(seconds=15)tick=True restarts real time from the given point, so results are not reproducible to the microsecond. auto_tick_seconds is deterministic and overrides tick, which is usually what you want for assertions.
Freeze with a timezone offsettimezone-offset
import datetime
from freezegun import freeze_time
@freeze_time("2012-01-14 03:21:34", tz_offset=-4)
def test_local_time():
assert datetime.datetime.now() == datetime.datetime(2012, 1, 13, 23, 21, 34)
assert datetime.date.today() == datetime.date(2012, 1, 13)
@freeze_time("2012-01-14 03:21:34", tz_offset=-datetime.timedelta(hours=3, minutes=30))
def test_half_hour_offset():
assert datetime.datetime.now() == datetime.datetime(2012, 1, 13, 23, 51, 34)The frozen string is treated as UTC and tz_offset shifts what local calls see. Offsets can be a whole number of hours or a timedelta for places like India and Newfoundland.
Wrap freeze_time in a pytest fixturepytest-fixture
import pytest
from freezegun import freeze_time
@pytest.fixture
def frozen_clock():
with freeze_time("2024-01-01 00:00:00") as frozen:
yield frozen
def test_daily_job(frozen_clock):
run_daily_job()
frozen_clock.tick(delta=datetime.timedelta(days=1))
run_daily_job()Yielding from inside the with block guarantees the clock is restored even if the test fails. Do not make this fixture session-scoped unless you want every test in the session frozen.
Exclude libraries that break under a freezeignore-packages
from freezegun import freeze_time
import freezegun
with freeze_time("2020-10-06", ignore=["threading", "tensorflow"]):
...
# or set it once for the whole test session:
freezegun.configure(extend_ignore_list=["tensorflow"])configure(default_ignore_list=...) replaces the built-in list (threading, selenium, gi, parts of pytest and more); extend_ignore_list adds to it. Replacing it by accident is a common cause of hangs.
Keep asyncio working while the clock is frozenasyncio-real-monotonic
import asyncio
import datetime
from freezegun import freeze_time
@freeze_time("2012-01-14", real_asyncio=True)
async def test_asyncio():
await asyncio.sleep(1)
assert datetime.datetime.now() == datetime.datetime(2012, 1, 14)Without real_asyncio=True the event loop sees a frozen time.monotonic(), so asyncio.sleep() and any timeout built on it never complete and the test hangs instead of failing.
Pass dates, callables, or generatorsflexible-input-formats
import datetime
from freezegun import freeze_time
with freeze_time("Jan 14th, 2012"): # parsed by dateutil
...
with freeze_time(lambda: datetime.datetime(2012, 1, 14)):
...
dates = (datetime.datetime(y, 1, 1) for y in range(2010, 2012))
with freeze_time(dates): # 2010-01-01
...
with freeze_time(dates): # 2011-01-01
...Generators advance one value per freeze_time call and raise StopIteration when exhausted. String parsing goes through python-dateutil, so ambiguous formats like 03/04/2012 follow dateutil's rules, not yours.
Know what freeze_time cannot reachwhat-stays-unfrozen
import datetime as dt
from freezegun import freeze_time
def report(default=dt.date.today()): # evaluated at import, not at call
print(default)
with freeze_time("2000-01-01"):
report() # prints today's real date
report(dt.date.today()) # prints 2000-01-01Default arguments are computed when the function is defined, so they escape the freeze. The same blind spot applies to clocks outside Python: SQL NOW(), Redis TTLs, and timestamps generated inside C extensions all stay on the real clock.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| time-machine | PyPI | Same idea, implemented as a C extension that patches the clock at the CPython level; pick it when freeze overhead shows up in your suite runtime or you want nested freezes to behave predictably. |
| pytest-freezer | PyPI | You want a pytest fixture wrapper around freezegun instead of decorators, so freezing is requestable per test like any other fixture. |
| libfaketime | PyPI | You need the fake clock to reach code outside Python, including C extensions and subprocesses, because it works via LD_PRELOAD rather than module patching. |