time-machine
time-machine changes the current time seen by Python's standard date and time functions by hooking them at the CPython C layer. Tests can travel with a decorator, sync or async context manager, unittest class decorator, pytest marker, or pytest fixture, then move or shift the clock. Unlike patching one import, it affects references throughout the interpreter, including functions captured before the test starts.
time-machine is an excellent CPython test tool when import-safe and fast clock changes matter. Its global interpreter effect is the reason it works well and the reason it is unsafe for tests that need isolated concurrent clocks.
Use it if
- A large CPython test suite needs faster time control than module-scanning patch libraries
- Code imports datetime and time functions in many places that are awkward to patch individually
- Tests need to move or shift time during one scenario rather than freeze a single instant
- You want pytest markers and fixtures plus direct decorator and context-manager forms
- Your test suite runs on PyPy or another Python interpreter; the installation guide says only CPython is supported because the package hooks the C API
- Concurrent threads or asynchronous tasks must see independent clocks; the usage guide warns that time is global state and all concurrent work in the interpreter is affected
- The system under test gets time from PostgreSQL, another process, or an external service; the docs state those processes continue to return real time
- You need every native library or operating-system time call changed; the comparison guide says libraries using their own system calls are a weak point
- You depend on freezegun features such as `auto_tick_seconds` or `tz_offset`; the migration guide says they are not supported and timezone behavior differs
Setup reality
`python -m pip install time-machine` installs version 3.3.1 for CPython 3.10 through 3.15, including free-threaded variants from 3.14. It is a C-extension technique, so unsupported interpreters are a hard stop rather than a missing extra. ISO-formatted strings work without another package; wider natural-language parsing needs `time-machine[dateutil]`, and the source-rewriting migration command needs `time-machine[cli]`. The first-run surprise is that `tick=True` is the default: the first observed value is the destination and later calls advance with elapsed real time. Pass `tick=False` for exact frozen assertions. The second is naive datetime interpretation. The default MIXED mode treats naive datetime objects and dates as UTC but naive strings as local time for backward compatibility. New projects should deliberately select LOCAL or ERROR in test setup. Travel changes time process-wide within the interpreter, so parallel threads, async tasks, background callbacks, and unrelated tests can observe it; scope travel tightly and avoid leaking manually started travellers. Database `NOW()`, child processes, and remote APIs are unaffected. A ZoneInfo-aware destination also changes the process timezone through `time.tzset()` only on Unix, and it does not change framework concepts such as Django's active timezone. pytest discovers the plugin automatically, which adds a `time_machine` fixture and marker that may conflict in name with the imported module unless aliases are clear.
Patterns
Freeze time inside a context managerfreeze-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)Pass `tick=False` for an exact frozen instant. The default is true, so later calls normally advance with real elapsed time.
Travel for one test functiondecorate-test
import datetime as dt
import time_machine
@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 avoids machine-dependent naive-time interpretation. The decorator restores real time when the function exits.
Apply travel with a pytest markeruse-pytest-marker
import datetime as dt
import pytest
@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 pytest plugin registers the marker automatically and applies travel during function-scoped fixture setup and teardown too.
Move and shift the pytest fixture clockmove-pytest-clock
import datetime as dt
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 change time until `move_to()` is called. It is function-scoped and exposes the Traveller movement methods.
Allow time to advance after traveltest-ticking-time
import time
import time_machine
with time_machine.travel(0.0, tick=True):
first = time.time()
do_work()
second = time.time()
assert second >= firstTicking advances by real elapsed duration after the first mocked call. Avoid exact equality assertions when tick is enabled.
Scope travel around async codetravel-async
import time
import time_machine
async def test_async_job():
async with time_machine.travel(1000.0, tick=False):
await run_job()
assert time.time() == 1000.0The context manager supports async syntax, but the clock is still shared with every concurrent task and thread in the interpreter.
Test local time with ZoneInfo on Unixchange-timezone
import datetime as dt
import time
from zoneinfo import ZoneInfo
import time_machine
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 == 16
assert time.tzname == ('PST', 'PDT')Timezone mocking calls `time.tzset()` and is Unix-only. It does not change Django's active timezone or an external service's timezone.
Reject naive travel destinationsset-naive-policy
import time_machine
# Put this in test bootstrap or conftest.py.
time_machine.naive_mode = time_machine.NaiveMode.ERRORERROR prevents dates and naive datetimes from silently changing meaning across machines. The default MIXED mode treats objects and strings differently.
Use real time for an external signatureaccess-real-time
import time_machine
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 time methods raise ValueError when travel is not active, which helps prevent accidental use in ordinary application code.
Manually manage travel with guaranteed cleanupmanual-start-stop
import time_machine
travel = time_machine.travel(1234.0, tick=False)
traveller = travel.start()
try:
assert traveller is not None
exercise_code()
finally:
travel.stop()Prefer a decorator or context manager. Manual nested travellers must be stopped in reverse order even when the test raises.
Travel relative to the current clockrelative-travel
import datetime as dt
import time_machine
with time_machine.travel(dt.timedelta(days=7), tick=False):
run_week_later_checks()A timedelta destination is relative to current time. An explicit aware datetime is usually more reproducible when the expected date matters.
Apply one clock to a unittest classdecorate-unittest-class
import time
import unittest
import time_machine
@time_machine.travel(0.0, tick=False)
class EpochTests(unittest.TestCase):
def test_epoch(self):
self.assertEqual(time.time(), 0.0)Class decoration is supported for unittest.TestCase subclasses and spans class setup through class teardown, not only individual test methods.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| freezegun | PyPI | You need its familiar API, wider interpreter approach, or unsupported migration features and can accept module-scanning overhead |
| pytest-freezer | PyPI | You want a pytest-focused fixture layer around frozen time rather than a broader direct API |
| libfaketime | PyPI | You need operating-system-level time interception and can manage Unix preload constraints |