time-machine

Travel through time in your Python tests by mocking datetime and time at the C level.

Library
PyPI
v3.5.0
996stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
75/100Good
Development Activity92
Maintenance52
Community56
Maturity60
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
90/100Excellent
Architecture88
Code Quality95
Innovation85
Learning Curve90

time-machine lets Python test suites travel to any point in time, mocking every standard-library function that returns the current date or datetime — datetime.datetime.now(), time.time(), time.localtime(), and more. Instead of patching each import location the way unittest.mock does, it replaces the underlying C function pointers inside the datetime and time modules directly, so every piece of code that reads the clock sees the mocked value regardless of how it imported the function.

Because the mocking happens at the C layer rather than by walking loaded modules (as freezegun does), it stays fast even in large codebases, and it also affects functions hidden inside class attributes or default arguments that module-search-based tools miss. It ships as a pytest plugin out of the box, contributing both a time_machine fixture and a @pytest.mark.time_machine(...) marker, and can just as easily be used as a decorator, a synchronous or asynchronous context manager, or a unittest.TestCase class decorator.

A companion CLI (installed via the time-machine[cli] extra) automates migrating existing freezegun test suites to time-machine’s API, rewriting decorators, fixtures, and imports across a codebase in one pass.

What You Get

  • C-level patching - swaps the function pointers behind datetime.now(), time.time(), time.localtime(), time.strftime(), and other clock-reading standard-library functions, so every caller sees the mocked time regardless of how it imported the function.
  • Pytest plugin built in - auto-registers a time_machine fixture and a @pytest.mark.time_machine(...) marker the moment the package is installed, no plugin configuration required.
  • Flexible destination types - accepts a datetime, date, timedelta, Unix timestamp, ISO date string, generator, or callable as the point in time to travel to.
  • Ticking or frozen time - tick=True (the default) keeps the mocked clock advancing in real time from the destination; tick=False freezes it exactly.
  • Timezone mocking - travelling to a timezone-aware datetime also calls time.tzset() to mock the process’s current timezone on Unix.
  • Escape hatch API - time_machine.escape_hatch exposes the real, unmocked datetime/time functions and an is_travelling() check, for code (like external-service authentication) that needs the genuine current time even mid-travel.
  • freezegun migration CLI - the optional time-machine migrate command rewrites a codebase’s freezegun decorators, fixtures, and imports to time-machine’s equivalents automatically.

Common Use Cases

  • Deterministic date-dependent tests - freeze datetime.now() to a fixed value so assertions about today(), ages, expirations, or scheduled jobs don’t depend on when the test runs.
  • Testing time-based business logic - simulate specific historical or future dates (subscription renewals, holiday pricing, daylight-saving transitions) without waiting for real time to pass.
  • Migrating off freezegun for speed - swap a slow freezegun-based suite to time-machine’s C-level mocking, using the bundled CLI to do most of the rewrite automatically.
  • Testing timezone-sensitive code - travel to a datetime with a zoneinfo.ZoneInfo attached to verify behavior under a specific local timezone via time.tzset().
  • Async test suites - use async with time_machine.travel(...) to mock time around asynchronous test functions and coroutines.

Under The Hood

Architecture time-machine’s core is a small CPython C extension (src/_time_machine.c) that stores original method pointers for the patched functions in process-wide statics, guarded by a platform-specific mutex (SRWLOCK on Windows, a POSIX mutex elsewhere) so patch()/unpatch() are safe across per-interpreter GILs and free-threaded builds. The Python-facing layer (src/time_machine/__init__.py) implements the public travel class as a context manager, decorator, and unittest.TestCase class decorator around this C module, tracking a per-interpreter traveller stack so nested travel() blocks unwind correctly even across exceptions. A separate cli.py module implements an independent migration tool, built on tokenize-rt, that rewrites freezegun call sites into time-machine equivalents via token-stream manipulation rather than an AST round-trip, preserving original formatting.

Tech Stack The package is pure CPython C plus Python (no non-test runtime dependencies), built via setuptools with cibuildwheel producing prebuilt wheels across CPython 3.10 through 3.15, including free-threaded builds. It registers itself as a pytest11 entry point so pytest auto-discovers its plugin, and optionally depends on python-dateutil (for flexible string-date parsing) and tokenize-rt (only for the CLI extra). Docs are built with Sphinx and the Furo theme, published to Read the Docs.

Code Quality The test suite is extensive relative to the library’s size — thousands of lines across test_time_machine.py, test_cli.py, plus dedicated Hypothesis-based fuzz tests (test_fuzz.py, test_cli_fuzz.py) that generate random inputs to catch edge cases in the C-level patching and the CLI’s token rewriting. CI runs the full matrix across every supported CPython version including free-threaded builds, and the project advertises 100% coverage. Typing is strict: the codebase is fully type-annotated and checked with mypy --strict, and linted/formatted with ruff, enforced via pre-commit.

What Makes It Unique time-machine’s explicit design goal is combining freezegun’s ergonomic API with the speed and completeness of C-level mocking tools like libfaketime, without requiring LD_PRELOAD or re-executing the test process. Because it patches the actual C function pointers behind the standard library rather than searching for and replacing every module-level import (freezegun’s approach), its mocking cost doesn’t scale with the number of loaded modules, and it correctly reaches functions hidden behind class attributes or default arguments that import-search tools cannot find. Its stated limitation is that it only covers CPython (not PyPy) and cannot affect compiled extensions, like NumPy or SQLite, that read the system clock directly rather than through the mocked standard-library calls.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search