time-machine Documentation
repository·main·Indexed 21 days ago
https://github.com/adamchainz/time-machineA Python library for mocking and manipulating system time during testing to enable deterministic testing of time-sensitive code. It supports CPython (versions 3.10 to 3.15) and provides a @time_machine.travel decorator, a pytest plugin with markers and fixtures, and a migration CLI for transitioning from freezegun. Unlike some alternatives, it mocks standard library functions at the C layer without requiring LD_PRELOAD.
What's inside time-machine
- time-machine is a Python library designed to manipulate time in tests. It allows developers to mock or travel to specific points in time, which is useful for testing time-dependent logic (e.g., expiration, scheduling, or time-zone sensitive code) without relying on the actual system clock.
Compare time-machine with other time-mocking libraries
mainWhen choosing a library for mocking time in Python, consider the following trade-offs between
time-machineand its alternatives:unittest.mock: Fragile because it only affects specific import locations. It cannot mock references like function default arguments (e.g.,def func(_now=time.time):).freezegun: Uses a find-and-replace mock strategy. It is slow in large projects because mocking time is proportional to the number of loaded modules. It also fails to find imports hidden inside objects (like class-level attributes) and cannot affect C extensions.python-libfaketime: A "perfect" mock that wraps C-level system calls, making it very fast and comprehensive. However, it relies onLD_PRELOAD, which is Unix-only and requires either re-executing the process (breaking debuggers/profilers) or manual environment variable management.time-machine: Combines the speed and comprehensiveness oflibfaketimewith the ease of use offreezegun. It works withoutLD_PRELOADand mocks standard library functions everywhere they are referenced.
Key Limitations of
time-machine:- It does not mock libraries that use direct C-level system calls for time (though these are rare and can sometimes be added to the mocked set).
- It is currently limited to CPython and does not support other interpreters like PyPy.
Configure naive datetime behavior with naive_mode
mainBy default,
time-machineuses mixed behavior for naive datetimes: naivedatetimeanddateobjects are interpreted as UTC, while naive datetime strings are interpreted as local time.To avoid surprising behavior in new projects, you should explicitly set
time_machine.naive_modeto one of the following modes:NaiveMode.LOCAL: Provides consistency with Python's default semantics (recommended when migrating fromfreezegun).NaiveMode.ERROR: Ensures maximum test reproducibility by failing on ambiguous naive datetimes.
Mock the current timezone on Unix
mainIf the
destinationpassed totravel()ormove_to()contains azoneinfo.ZoneInfoinstance,time-machinewill attempt to mock the current system timezone usingtime.tzset().Note: This functionality is only available on Unix systems. It affects
time.localtime()and other features relying ontimemodule timezone constants, but it does not affect high-level abstractions like Django'stimezone.override().import datetime as dt import time from zoneinfo import ZoneInfo import time_machine hill_valley_tz = ZoneInfo("America/Los_Angeles") @time_machine.travel(dt.datetime(2015, 10, 21, 16, 29, tzinfo=hill_valley_tz)) def test_hoverboard_era(): # time.tzname is updated via time.tzset() assert time.tzname == ("PST", "PDT") now = dt.datetime.now() assert (now.hour, now.minute) == (16, 29)Understand the global nature of time-machine's time mocking
mainWhen using
time-machineto mock time, be aware that time is treated as a global state. This has several critical implications for your application:- Concurrency: All concurrent threads and asynchronous functions within the same process are affected by the mocked time. Some functions or libraries may crash or behave unexpectedly if time moves too rapidly or backwards.
- Process Isolation: Time mocking only affects the current process. Other processes (e.g., a separate database server) will continue to return the real system time.
- Interpreter Isolation: If using
concurrent.interpreters, time travelling only affects the specific interpreter where the patched functions are called. Interpreters that are not actively travelling will fall back to original functions and return the real time.
Quickstart: Mock time in tests with @time_machine.travel
mainYou can mock the current time in your tests by using the
@time_machine.traveldecorator. This allows you to simulate specific dates and times, including timezone-aware datetimes, within the scope of a test function.To use it, import
time_machineand apply the decorator to your test function, passing adatetime.datetimeobject as the argument.import datetime as dt from zoneinfo import ZoneInfo import time_machine hill_valley_tz = ZoneInfo("America/Los_Angeles") @time_machine.travel(dt.datetime(1985, 10, 26, 1, 24, tzinfo=hill_valley_tz)) def test_delorean(): assert dt.date.today().isoformat() == "1985-10-26"Use the `time_machine` pytest fixture to control time
mainThe
time_machinefixture is a function-scoped fixture that provides programmatic control over time within a test. It provides an object with two primary methods:move_to(destination): Sets the current time to the specified destination.shift(delta): Shifts the current time by the specifiedtimedelta.
Note: Time is not mocked until you explicitly call
move_to()orshift()using the fixture object.import datetime as dt def test_delorean(time_machine): # Time is not mocked yet # Set specific time time_machine.move_to(dt.datetime(1985, 10, 26)) assert dt.date.today().isoformat() == "1985-10-26" # Change to another time time_machine.move_to(dt.datetime(2015, 10, 21)) assert dt.date.today().isoformat() == "2015-10-21" # Shift time by one day time_machine.shift(dt.timedelta(days=1)) assert dt.date.today().isoformat() == "2015-10-22"Requirements for time-machine
mainTo use
time-machine, ensure your environment meets the following requirements:- Python Version: Python 3.10 to 3.15 (including free-threaded variants from Python 3.14 onwards).
- Implementation: Only CPython is supported because the library hooks directly into the C-level API.
Mock time using `travel()` as a decorator or context manager
mainYou can use
time_machine.travel()in several ways to ensure time is automatically restored after the block or function ends:Function Decorator
Works for both synchronous and asynchronous functions.
Context Manager
Use
with(sync) orasync with(async) to mock time during a specific block of code.Class Decorator
Only supports
unittest.TestCasesubclasses. Time is mocked from the start ofsetUpClass()to the end oftearDownClass().import time import time_machine # Function decorator @time_machine.travel("1970-01-01 00:00 +0000") def test_in_the_deep_past(): assert 0.0 < time.time() < 1.0 # Async function decorator @time_machine.travel("1970-01-01 00:00 +0000") async def test_async_past(): assert 0.0 < time.time() < 1.0 # Synchronous context manager def test_context_manager(): with time_machine.travel(0.0): assert 0.0 < time.time() < 1.0 # Asynchronous context manager async def test_async_context_manager(): async with time_machine.travel(0.0): assert 0.0 < time.time() < 1.0Install time-machine via pip
mainYou can install
time-machineusingpip. Depending on your needs, you can install the base package or include optional extras for string parsing or CLI support.# Base installation python -m pip install time-machine # With support for parsing strings as datetimes python -m pip install time-machine[dateutil] # With support for the migration CLI python -m pip install time-machine[cli]Migrating from freezegun or libfaketime to time-machine
mainIf you are performing simple function calls, you can migrate by replacing calls to
freezegun.freeze_time()orlibfaketime.fake_time()withtime_machine.travel().Key API Differences
tickargument: Intime-machine, thetickargument intravel()defaults toTrue(time progresses while running). Infreezegun, the default behavior is effectivelyFalse. To matchfreezegun's behavior, you must explicitly passtick=False.- Timezone Handling:
freezeguninterprets naive datetimes in the local time zone.time-machineinterprets naive datetimes in UTC. To mock a specific time zone, provide adatetimeobject with aZoneInfotimezone. - Shifting Time:
freezegun'stick()method is implemented asshift()intime-machine. Unlikefreezegun,shift()requires an explicit delta. - Unsupported Arguments:
freezegun'stz_offsetandauto_tick_secondsare not supported bytime-machine.
# Example migration pattern # From freezegun: # @freeze_time("2023-01-01") # def test_func(): ... # To time-machine: # @time_machine.travel("2023-01-01", tick=False) # def test_func(): ...Use the `time_machine` pytest marker to mock time
mainThe
time_machinemarker allows you to mock the time for a specific test function or an entire test class. When applied, the time is mocked for the duration of the test, including any setup or teardown code performed by function-scoped fixtures.To use it, pass a valid destination (such as a
datetime.datetimeobject) to the marker.import datetime as dt import pytest # Mocking a single test function @pytest.mark.time_machine(dt.datetime(1985, 10, 26)) def test_delorean_marker(): assert dt.date.today().isoformat() == "1985-10-26" # Mocking an entire class of tests @pytest.mark.time_machine(dt.datetime(1985, 10, 26)) class TestSomething: def test_one(self): assert dt.date.today().isoformat() == "1985-10-26" def test_two(self): assert dt.date.today().isoformat() == "1985-10-26"