time-machine review
time-machine changes what Python's standard `datetime` and `time` functions report by hooking CPython at the C layer. Tests can travel through a decorator, sync or async context manager, pytest marker, pytest fixture, or manually controlled Traveller, then move or shift the clock. That interpreter-level hook reaches functions imported before travel starts. Version 3.5.0 adds `None` as a destination for `travel()` and `move_to()`, which freezes the actual current instant when paired with `tick=False`.
time-machine 3.4.0 installed in 0.2 seconds and imported its compiled hook in 0.02 seconds on our CPython box; current 3.5.0 now adds a `None` destination for freezing the present. Install it for fast process-wide clock control, and avoid it when concurrent tests need isolated clocks or the time source lives outside Python.
We installed it
| Install | ✓ · 0.2s | 1 package on disk · 1 MB |
| Import | ✓ | import _time_machine in 0.02s · compiled extensions · py.typed · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does time-machine install cleanly?
Yes. In a fresh container with an empty cache, pip install time-machine finished in 0.2s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does time-machine need to run?
Python >=3.10, and a platform wheel with compiled extensions. In our run import _time_machine succeeded in 0.02s, and the package ships py.typed for type checkers.
time-machine or freezegun: which should you use?
freezegun: Use it for its familiar patching API, wider interpreter approach, or migration features absent here. time-machine 3.4.0 installed in 0.2 seconds and imported its compiled hook in 0.02 seconds on our CPython box; current 3.5.0 now adds a None destination for freezing the present.
When should you not use time-machine?
Your suite runs on PyPy or another interpreter. The project supports CPython because its mechanism depends on CPython C APIs.
Use it if
- A CPython test suite imports date and time functions across many modules and patching every lookup is brittle.
- One scenario needs to jump or shift the clock several times while retaining a clear cleanup scope.
- Tests use pytest, unittest, ordinary decorators, or async context managers and need the same clock tool.
- Runtime speed matters enough to accept a compiled CPython extension and process-global clock state.
- Your suite runs on PyPy or another interpreter. The project supports CPython because its mechanism depends on CPython C APIs.
- Concurrent threads or async tasks require different clocks. Travel affects the interpreter globally, so overlapping work observes the same altered time.
- The time source is a database, child process, remote API, or native library that calls the operating system independently. Those sources remain real.
- Tests rely on freezegun's `auto_tick_seconds` or `tz_offset`. The migration guide identifies behavior differences and does not provide those options directly.
- Your build cannot consume compiled wheels or compile an extension. Our 3.4.0 package inspection found a shipped `.so`, so this is not a pure-Python fallback.
Setup reality
We installed time-machine 3.4.0 in a clean Python 3.12 Bookworm container on August 22, 2026. The install finished in 0.2 seconds, left 1 package using 1 MB, and import _time_machine worked in 0.02 seconds. That measured release declared 2 direct dependencies, required Python 3.10 or newer, shipped py.typed plus a compiled .so, and produced 0 known findings in pip-audit. Its package metadata did not give us a license value; the GitHub repository declares MIT.
PyPI moved to 3.5.0 on August 25, after our sandbox run, so the install figures above describe 3.4.0 rather than the current wheel. Version 3.5.0 accepts None in travel() and Traveller.move_to(). With tick=False, that captures and freezes the present moment. Standard ISO strings need no extra parser; broader text parsing uses the dateutil extra, and the freezegun migration command has its own CLI extra.
The first behavioral surprise is tick=True, the default. The destination is returned first and subsequent calls advance with real elapsed time, so exact equality assertions need tick=False. Naive destinations also need a policy: the compatibility-oriented MIXED mode treats naive objects and strings differently. Set NaiveMode.ERROR or another deliberate mode in test bootstrap to prevent results changing with local machine timezone.
Travel is global inside the interpreter. Threads, concurrent tasks, background callbacks, and unrelated tests can see it until the context exits. Databases and child processes do not. An aware ZoneInfo destination may also change process timezone through time.tzset() on Unix; it does not change framework-level timezone settings. Prefer decorators and context managers, and if manual start() is unavoidable, stop nested travellers in reverse order inside finally.
Patterns
Freeze an exact UTC instant freeze-context
import datetime as dt
import time_machine
with time_machine.travel('2025-01-15 12:00:00+00:00', tick=False):
assert dt.datetime.now(dt.UTC) == dt.datetime(2025, 1, 15, 12, tzinfo=dt.UTC)Set `tick=False` for equality assertions. The default value is true, which advances the travelled clock with real elapsed time.
Freeze the present instant in 3.5.0 freeze-current-time
import datetime as dt
import time_machine
with time_machine.travel(None, tick=False):
captured = dt.datetime.now(dt.UTC)
assert dt.datetime.now(dt.UTC) == capturedA `None` destination was added in 3.5.0. Pin that minimum version wherever tests use this form.
Apply one clock to a test function decorate-test
@time_machine.travel(dt.datetime(1985, 10, 26, tzinfo=dt.UTC), tick=False)
def test_release_date():
assert dt.date.today() == dt.date(1985, 10, 26)An aware datetime removes local-zone ambiguity, and the decorator restores the prior clock when the function exits.
Travel during pytest setup and test execution use-pytest-marker
@pytest.mark.time_machine(
dt.datetime(2015, 10, 21, tzinfo=dt.UTC),
tick=False,
)
def test_future_message():
assert dt.date.today().isoformat() == '2015-10-21'The installed plugin registers this marker and applies travel during function-scoped fixture setup and teardown as well.
Move the pytest fixture between assertions move-pytest-clock
def test_expiry(time_machine):
time_machine.move_to(dt.datetime(2026, 1, 1, tzinfo=dt.UTC), tick=False)
assert not subscription.expired()
time_machine.shift(dt.timedelta(days=31))
assert subscription.expired()The fixture does not alter time until `move_to()` runs. It is function-scoped and exposes Traveller methods.
Let elapsed real time advance the destination test-ticking-time
with time_machine.travel(0.0, tick=True):
first = time.time()
do_work()
second = time.time()
assert second >= firstTicking begins from the destination and then follows elapsed wall time. Assertions should use ordering or a tolerance.
Enter travel with an async context manager travel-async
async def test_async_job():
async with time_machine.travel(1000.0, tick=False):
await run_job()
assert time.time() == 1000.0Async syntax controls cleanup only. Every task and thread in the interpreter still observes the same travelled clock.
Test local time in a Unix process change-timezone
la = ZoneInfo('America/Los_Angeles')
with time_machine.travel(
dt.datetime(2015, 10, 21, 16, 29, tzinfo=la),
tick=False,
):
assert dt.datetime.now().hour == 16Timezone switching uses `time.tzset()` and is Unix-only. It does not update Django's active timezone or remote systems.
Reject naive destinations in the suite set-naive-policy
# conftest.py or test bootstrap
time_machine.naive_mode = time_machine.NaiveMode.ERRORThe default MIXED compatibility mode interprets naive objects and naive strings differently. ERROR makes accidental ambiguity fail early.
Read real time inside a travelled scope access-real-time
with time_machine.travel('2001-01-01', tick=False):
real_now = time_machine.escape_hatch.datetime.datetime.now()
signature = sign_external_request(now=real_now)Escape-hatch functions raise `ValueError` outside active travel, which helps keep them out of ordinary application paths.
Guarantee cleanup for manual travel manual-start-stop
travel = time_machine.travel(1234.0, tick=False)
traveller = travel.start()
try:
exercise_code()
finally:
travel.stop()Manual travellers can leak global time after a failure. A context manager is safer; nested manual starts must stop in reverse order.
Move one week from the current clock relative-travel
with time_machine.travel(dt.timedelta(days=7), tick=False):
run_week_later_checks()A timedelta destination is relative to the clock at entry. Use an aware datetime when a fixed calendar result is the assertion.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| freezegun | PyPI | Use it for its familiar patching API, wider interpreter approach, or migration features absent here. |
| pytest-freezer | PyPI | Use it when a pytest fixture is the main interface and direct Traveller control is unnecessary. |
| libfaketime | PyPI | Use it when Unix preload-based interception must reach beyond Python's standard date and time calls. |
More testing guides
pytest · chai · jsdom · vitest · playwright · coverage · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

