time-machine Documentation

repository·main·Indexed 21 days ago

https://github.com/adamchainz/time-machine

A 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.

Tokens
7.6K
Snippets
23
Records
29
Agent score
75%

What's inside time-machine

  1. Overview of time-machine

    main
    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.
  2. Compare time-machine with other time-mocking libraries

    main

    When choosing a library for mocking time in Python, consider the following trade-offs between time-machine and 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 on LD_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 of libfaketime with the ease of use of freezegun. It works without LD_PRELOAD and 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.
  3. Configure naive datetime behavior with naive_mode

    main

    By default, time-machine uses mixed behavior for naive datetimes: naive datetime and date objects 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_mode to one of the following modes:

    • NaiveMode.LOCAL: Provides consistency with Python's default semantics (recommended when migrating from freezegun).
    • NaiveMode.ERROR: Ensures maximum test reproducibility by failing on ambiguous naive datetimes.
  4. Mock the current timezone on Unix

    main

    If the destination passed to travel() or move_to() contains a zoneinfo.ZoneInfo instance, time-machine will attempt to mock the current system timezone using time.tzset().

    Note: This functionality is only available on Unix systems. It affects time.localtime() and other features relying on time module timezone constants, but it does not affect high-level abstractions like Django's timezone.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)
  5. Understand the global nature of time-machine's time mocking

    main

    When using time-machine to mock time, be aware that time is treated as a global state. This has several critical implications for your application:

    1. 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.
    2. 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.
    3. 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.
  6. Quickstart: Mock time in tests with @time_machine.travel

    main

    You can mock the current time in your tests by using the @time_machine.travel decorator. 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_machine and apply the decorator to your test function, passing a datetime.datetime object 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"
  7. Use the `time_machine` pytest fixture to control time

    main

    The time_machine fixture 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 specified timedelta.

    Note: Time is not mocked until you explicitly call move_to() or shift() 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"
  8. Requirements for time-machine

    main

    To 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.
  9. Mock time using `travel()` as a decorator or context manager

    main

    You 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) or async with (async) to mock time during a specific block of code.

    Class Decorator

    Only supports unittest.TestCase subclasses. Time is mocked from the start of setUpClass() to the end of tearDownClass().

    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.0
  10. Install time-machine via pip

    main

    You can install time-machine using pip. 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]
  11. Migrating from freezegun or libfaketime to time-machine

    main

    If you are performing simple function calls, you can migrate by replacing calls to freezegun.freeze_time() or libfaketime.fake_time() with time_machine.travel().

    Key API Differences

    • tick argument: In time-machine, the tick argument in travel() defaults to True (time progresses while running). In freezegun, the default behavior is effectively False. To match freezegun's behavior, you must explicitly pass tick=False.
    • Timezone Handling: freezegun interprets naive datetimes in the local time zone. time-machine interprets naive datetimes in UTC. To mock a specific time zone, provide a datetime object with a ZoneInfo timezone.
    • Shifting Time: freezegun's tick() method is implemented as shift() in time-machine. Unlike freezegun, shift() requires an explicit delta.
    • Unsupported Arguments: freezegun's tz_offset and auto_tick_seconds are not supported by time-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(): ...
  12. Use the `time_machine` pytest marker to mock time

    main

    The time_machine marker 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.datetime object) 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"