Back to Testing guides

Time-machine vs Freezegun

Stanley Ulili
Updated on April 25, 2025

Testing time-dependent code in Python presents a unique challenge. You need to simulate specific dates and times without actually waiting or changing your system clock. Python offers two standout libraries to solve this problem.

time-machine brings modern performance to time mocking. Built with C extensions, it offers speed and precision for complex testing scenarios. This newer library excels in performance-critical applications.

freezegun is a mature tool in the Python testing world. With its intuitive API and years of community adoption, it prioritizes simplicity and ease of use over raw performance.

This comparison will help you understand the strengths of each library, enabling you to choose the right tool for your specific testing needs.

What is freezegun?

Screenshot of freezegun Github page

freezegun remains the most recognized time mocking library in the Python ecosystem. Created by Steve Pulec back in 2012, it quickly became the standard solution for time-dependent testing.

The library operates by replacing (or "patching") Python's built-in time functions. You'll find it remarkably simple to use as either a decorator or context manager to set a specific time for your tests without complex configuration.

Simplicity drives freezegun's popularity. The name perfectly captures its core function - it freezes time at exactly the point you specify. While speed isn't its primary strength, you'll benefit from its widespread adoption. Nearly every Python testing tutorial or Stack Overflow thread about time mocking references this library, ensuring you'll always find help when needed.

What is time-machine?

Screenshot of time-machine Github page

time-machine represents a modern approach to time mocking. Created by Adam Johnson in 2020, this library leverages C extensions to modify time at a fundamental level - directly in the Python interpreter itself.

The technical implementation yields impressive performance gains over pure Python alternatives. Beyond simply freezing time, time-machine provides sophisticated control to advance time forward in precise increments during your tests.

For applications where testing speed matters or when dealing with complex time-dependent logic, time-machine's performance advantages and precise time controls offer compelling benefits. Despite being relatively new to the Python testing ecosystem, it addresses performance limitations that have long affected time mocking libraries.

time-machine vs freezegun: a quick comparison

The differences between these libraries become clearer when examining their core attributes side by side:

Feature time-machine freezegun
Implementation approach C extension with direct interpreter integration Pure Python implementation using module patching
Performance Very fast with minimal overhead Slower due to pure Python implementation
API style Context manager and decorator with travel options Decorator and context manager with freeze focus
Time travel capabilities Support for both freezing and moving time forward Primarily designed for freezing time
Ease of use Simple API with explicit time movement functions Intuitive API focused on setting specific times
Installation complexity Requires compiler for C extension Pure Python, simpler installation
Compatibility Python 3.7+ with some platform limitations Broad Python version support (2.7+ and 3.5+)
Memory usage Lower due to C implementation Higher due to Python object overhead
Community adoption Growing, newer project Widespread, mature ecosystem
Auto-advance features Native support for incremental time advancement Limited automatic time progression
Maintenance activity Active development, frequent updates Stable but less frequent updates
Testing framework integration Works with pytest, unittest, and others Well-established integration with major frameworks
Timezone handling Comprehensive timezone support Good timezone support with some edge cases
Thread safety Thread-local time mocking available Some limitations with multithreaded code

Installation and setup

Setting up your time mocking library should be quick and painless, letting you focus on writing tests instead of configuration. The setup process reveals key differences between these tools.

freezegun offers effortless installation thanks to its pure Python implementation:

 
pip install freezegun

This approach eliminates concerns about compilers or platform-specific issues. The library works consistently across all Python environments, making it particularly valuable for teams with diverse operating systems.

Implementing freezegun in your tests feels equally straightforward:

 
from freezegun import freeze_time
import datetime

@freeze_time("2023-01-01")
def test_new_years_day():
    assert datetime.datetime.now() == datetime.datetime(2023, 1, 1)

time-machine appears similar at first glance:

 
pip install time-machine

The difference lies beneath the surface. Since time-machine uses C extensions, you may need a compiler on certain systems. Most modern development environments handle this automatically, but it's worth considering for CI/CD pipelines or containerized deployments.

After installation, the basic usage patterns look familiar:

 
import time_machine
import datetime

@time_machine.travel("2023-01-01")
def test_new_years_day():
    assert datetime.datetime.now() == datetime.datetime(2023, 1, 1)

The terminology difference is subtle but meaningful: time-machine uses "travel" instead of "freeze_time," reflecting its expanded time manipulation capabilities.

Both libraries provide flexible input options, accepting string dates, datetime objects, and timestamps, giving you multiple ways to specify times in your tests.

Time freezing approaches

The fundamental purpose of these libraries centers on pausing time at a specific moment. Each library approaches this core functionality with distinctive philosophies.

freezegun embraces its namesake purpose with laser-focused simplicity:

 
from freezegun import freeze_time
import datetime

# As a decorator
@freeze_time("2023-05-21")
def test_specific_date():
    assert datetime.datetime.now().date() == datetime.date(2023, 5, 21)

# As a context manager
def test_with_context():
    with freeze_time("2023-05-21 12:30:00"):
        assert datetime.datetime.now().hour == 12
        assert datetime.datetime.now().minute == 30

The library also supports relative time freezing based on the current moment:

 
from freezegun import freeze_time
import datetime

# Freeze time to 2 days in the future
two_days_from_now = datetime.datetime.now() + datetime.timedelta(days=2)
with freeze_time(two_days_from_now):
    # Your test code here
    pass

time-machine provides comparable freezing functionality but conceptualizes it differently as "travel":

 
import time_machine
import datetime

# As a decorator
@time_machine.travel("2023-05-21")
def test_specific_date():
    assert datetime.datetime.now().date() == datetime.date(2023, 5, 21)

# As a context manager
def test_with_context():
    with time_machine.travel("2023-05-21 12:30:00"):
        assert datetime.datetime.now().hour == 12
        assert datetime.datetime.now().minute == 30

The distinctive advantage of time-machine emerges in its approach to time progression during tests:

 
import time_machine
import datetime
import time

def test_time_progression():
    traveler = time_machine.travel(datetime.datetime(2023, 1, 1))
    traveler.start()

    assert datetime.datetime.now().date() == datetime.date(2023, 1, 1)

    # Move time forward by 2 days
    traveler.shift(datetime.timedelta(days=2))
    assert datetime.datetime.now().date() == datetime.date(2023, 1, 3)

    # Jump to a specific future time
    traveler.move_to(datetime.datetime(2023, 6, 1))
    assert datetime.datetime.now().month == 6

    traveler.stop()

This precise control proves invaluable when testing expiration logic, scheduled tasks, or any code where time progression affects behavior.

Time travel and movement

Beyond simple time freezing, many testing scenarios require observing how code behaves as time progresses. The libraries diverge significantly in their capabilities here.

freezegun provides a basic auto-advancing mechanism through its tick parameter:

 
from freezegun import freeze_time
import datetime

# Auto-tick time forward by 1 second with each now() call
with freeze_time("2023-01-01", tick=True):
    first = datetime.datetime.now()
    second = datetime.datetime.now()
    third = datetime.datetime.now()

    # Each call advances by 1 second
    assert (second - first).total_seconds() == 1
    assert (third - second).total_seconds() == 1

# Custom tick increment (30 minutes)
with freeze_time("2023-01-01", tick=datetime.timedelta(minutes=30)):
    first = datetime.datetime.now()
    second = datetime.datetime.now()

    # Each call advances by 30 minutes
    assert (second - first).total_seconds() == 1800

The limitation becomes apparent quickly: time advances only when your code calls datetime.now(), creating unpredictable behavior in complex testing scenarios.

time-machine takes time manipulation to another level with explicit control methods:

 
import time_machine
import datetime
import time

def test_explicit_time_control():
    # Start at a specific time
    traveler = time_machine.travel(datetime.datetime(2023, 1, 1, 12, 0, 0))
    traveler.start()

    initial_time = datetime.datetime.now()
    assert initial_time.hour == 12

    # Explicitly move forward 3 hours
    traveler.shift(datetime.timedelta(hours=3))
    assert datetime.datetime.now().hour == 15

    # Jump to a specific future date and time
    future = datetime.datetime(2023, 5, 15, 9, 30, 0)
    traveler.move_to(future)
    assert datetime.datetime.now() == future

    traveler.stop()

A particularly valuable feature allows testing code with sleep() calls without actual waiting:

 
import time_machine
import datetime
import time

@time_machine.travel("2023-01-01", tick=True)
def test_sleep_with_tick():
    start = datetime.datetime.now()
    time.sleep(60)  # This won't actually wait
    end = datetime.datetime.now()

    # Time appears to have advanced by the sleep duration
    assert (end - start).total_seconds() == 60

This sophisticated control makes time-machine exceptionally powerful for testing complex time-dependent behaviors without slowing down your test execution.

Scope and patching

The interaction between these libraries and Python's time-related modules significantly affects their compatibility with your code and third-party dependencies.

freezegun takes a targeted approach to patching common time functions:

 
from freezegun import freeze_time
import datetime
import time

@freeze_time("2023-01-01")
def test_patching_scope():
    # datetime module is patched
    assert datetime.datetime.now() == datetime.datetime(2023, 1, 1)
    assert datetime.date.today() == datetime.date(2023, 1, 1)

    # time module is also patched
    assert time.time() == datetime.datetime(2023, 1, 1).timestamp()

    # Even direct calls to time.gmtime() are affected
    current_gmtime = time.gmtime()
    assert current_gmtime.tm_year == 2023
    assert current_gmtime.tm_mon == 1
    assert current_gmtime.tm_mday == 1

One standout capability in freezegun is selective module patching:

 
from freezegun import freeze_time
import datetime
import time

# Only patch datetime, not time
with freeze_time("2023-01-01", ignore=["time"]):
    # datetime is frozen
    assert datetime.datetime.now() == datetime.datetime(2023, 1, 1)

    # time module keeps using real time
    # (this assertion would fail as real time continues)
    # assert time.time() == datetime.datetime(2023, 1, 1).timestamp()

This granular control proves invaluable when working with finicky third-party libraries that might break under complete time mocking.

time-machine employs a more fundamental approach by integrating directly with the Python interpreter:

 
import time_machine
import datetime
import time

@time_machine.travel("2023-01-01")
def test_comprehensive_patching():
    # datetime module is patched
    assert datetime.datetime.now() == datetime.datetime(2023, 1, 1)
    assert datetime.date.today() == datetime.date(2023, 1, 1)

    # time module is patched
    assert time.time() == datetime.datetime(2023, 1, 1).timestamp()

    # gmtime and other functions are patched
    current_gmtime = time.gmtime()
    assert current_gmtime.tm_year == 2023
    assert current_gmtime.tm_mon == 1
    assert current_gmtime.tm_mday == 1

The C-level integration allows time-machine to intercept time-related calls with remarkable consistency, capturing calls made by third-party libraries or extension modules. The tradeoff comes in flexibility - unlike freezegun, selective patching isn't available, making it an all-or-nothing approach to time mocking.

Testing framework integration

Seamless integration with testing frameworks can dramatically improve your development workflow. Fortunately, both libraries integrate effectively with popular testing tools.

freezegun offers familiar integration patterns with pytest:

 
# pytest integration with freezegun
import pytest
from freezegun import freeze_time
import datetime

# Apply to individual test
@freeze_time("2023-01-01")
def test_new_year():
    assert datetime.datetime.now().day == 1
    assert datetime.datetime.now().month == 1

# Apply to entire class
@freeze_time("2023-01-01")
class TestDateRelated:
    def test_new_year(self):
        assert datetime.datetime.now().day == 1

    def test_calculations(self):
        # All methods in the class use the same frozen time
        assert datetime.datetime.now().year == 2023

Custom fixtures provide additional flexibility for complex testing scenarios:

 
import pytest
from freezegun import freeze_time
import datetime

# Custom fixture providing frozen time
@pytest.fixture
def frozen_january():
    with freeze_time("2023-01-15") as frozen:
        yield frozen

def test_with_fixture(frozen_january):
    assert datetime.datetime.now().month == 1

    # Move time forward
    frozen_january.move_to("2023-02-01")
    assert datetime.datetime.now().month == 2

time-machine maintains similar integration patterns while leveraging its unique capabilities:

 
# pytest integration with time-machine
import pytest
import time_machine
import datetime

# Apply to individual test
@time_machine.travel("2023-01-01")
def test_new_year():
    assert datetime.datetime.now().day == 1
    assert datetime.datetime.now().month == 1

# Apply to entire class
class TestDateRelated:
    @time_machine.travel("2023-01-01")
    def test_new_year(self):
        assert datetime.datetime.now().day == 1

    @time_machine.travel("2023-01-01")
    def test_calculations(self):
        assert datetime.datetime.now().year == 2023

Fixture creation follows established pytest patterns:

 
import pytest
import time_machine
import datetime

@pytest.fixture
def time_traveler():
    traveler = time_machine.travel(datetime.datetime(2023, 1, 15))
    traveler.start()
    yield traveler
    traveler.stop()

def test_with_traveler(time_traveler):
    assert datetime.datetime.now().month == 1

    # Move time forward
    time_traveler.shift(datetime.timedelta(days=30))
    assert datetime.datetime.now().month == 2

A subtle advantage of time-machine lies in its C implementation, which works more transparently with pytest's assertion rewriting, resulting in more informative error messages when tests fail.

Both libraries integrate smoothly with Python's standard unittest framework:

 
# unittest with freezegun
import unittest
from freezegun import freeze_time
import datetime

class TestWithFreezegun(unittest.TestCase):
    @freeze_time("2023-01-01")
    def test_frozen_date(self):
        self.assertEqual(datetime.date.today(), datetime.date(2023, 1, 1))

# unittest with time-machine
import unittest
import time_machine
import datetime

class TestWithTimeMachine(unittest.TestCase):
    @time_machine.travel("2023-01-01")
    def test_travel_date(self):
        self.assertEqual(datetime.date.today(), datetime.date(2023, 1, 1))

The similar usage patterns make switching between libraries relatively painless, allowing you to transition without significant disruption to your testing workflow.

Performance considerations

Performance differences become increasingly significant as your test suite grows. The speed of your time mocking solution can dramatically impact overall test execution time.

freezegun's pure Python implementation prioritizes compatibility over speed. This design choice introduces overhead with each time-related function call:

 
from freezegun import freeze_time
import datetime
import time

def test_freezegun_performance():
    # Start timing
    start = time.perf_counter()

    with freeze_time("2023-01-01"):
        # Make many datetime calls
        for _ in range(100000):
            datetime.datetime.now()

    # End timing
    end = time.perf_counter()
    print(f"Time taken: {end - start:.4f} seconds")

The performance impact becomes most noticeable in specific scenarios: - Test suites with extensive time mocking usage - Code making frequent time function calls - CI/CD environments where execution speed affects deployment times

time-machine's C extension architecture delivers substantial performance benefits:

 
import time_machine
import datetime
import time

def test_timemachine_performance():
    # Start timing
    start = time.perf_counter()

    with time_machine.travel("2023-01-01"):
        # Make many datetime calls
        for _ in range(100000):
            datetime.datetime.now()

    # End timing
    end = time.perf_counter()
    print(f"Time taken: {end - start:.4f} seconds")

Benchmark comparisons typically show time-machine performing 10-100 times faster than freezegun. This dramatic improvement stems from:

  1. Direct interception of time calls at the C level
  2. Elimination of Python-level function wrapping overhead
  3. More efficient time calculation algorithms

For smaller projects, this performance gap may seem negligible. However, in large codebases with comprehensive test coverage, time-machine's speed advantage translates to meaningful time savings during development.

An interesting side benefit: time-machine's minimal overhead can unmask actual performance issues in your application code that might otherwise be obscured by freezegun's inherent slowdown.

Advanced features and edge cases

Complex testing scenarios often require specialized time manipulation features. Both libraries offer advanced capabilities for handling edge cases.

freezegun provides several configuration options for precise control:

 
from freezegun import freeze_time
import datetime

# Auto-detect timezone from string
with freeze_time("2023-01-01 12:00:00 -0500"):
    # Will respect the timezone from the string (-0500)
    pass

# Specify timezone explicitly
nyc_timezone = datetime.timezone(datetime.timedelta(hours=-5))
with freeze_time("2023-01-01", tz_offset=nyc_timezone):
    # Will use NYC timezone
    pass

# Ignore certain modules
with freeze_time("2023-01-01", ignore=["django.utils.timezone"]):
    # Django's timezone module will continue using real time
    pass

Daylight saving time transitions represent particularly challenging test scenarios that freezegun can handle:

 
from freezegun import freeze_time
import datetime

# Test behavior during DST transition
spring_forward = datetime.datetime(2023, 3, 12, 1, 55, 0)  # Just before DST in US
with freeze_time(spring_forward) as frozen_time:
    initial = datetime.datetime.now()

    # Move forward 10 minutes, crossing DST transition
    frozen_time.tick(datetime.timedelta(minutes=10))
    after = datetime.datetime.now()

    # Time advanced by 10 minutes, but only 9 minutes in wall clock time
    # due to the "spring forward" DST change
    delta = after - initial
    print(f"Difference: {delta.total_seconds() / 60} minutes")

time-machine excels with specialized features for dynamic time control:

 
import time_machine
import datetime
import time

# Continuous time movement (real time passes at normal speed)
with time_machine.travel(datetime.datetime(2023, 1, 1), tick=True):
    start = datetime.datetime.now()
    time.sleep(5)  # Actual sleep
    end = datetime.datetime.now()

    # Real time has passed, but from a different starting point
    assert (end - start).total_seconds() >= 5

# Destination can be a callable for dynamic time setting
def get_tomorrow():
    return datetime.datetime.now() + datetime.timedelta(days=1)

with time_machine.travel(get_tomorrow):
    # Always one day ahead of the actual time
    pass

Thread isolation capabilities represent another unique strength of time-machine:

 
import time_machine
import datetime
import threading
import time

def worker():
    # This thread sees real time
    print(f"Worker thread time: {datetime.datetime.now()}")

def test_thread_isolation():
    # Main thread has mocked time
    with time_machine.travel("2023-01-01", tick=False):
        print(f"Main thread time: {datetime.datetime.now()}")

        # Start a worker thread
        thread = threading.Thread(target=worker)
        thread.start()
        thread.join()

Each library addresses advanced testing needs differently - time-machine provides superior control over time progression dynamics, while freezegun delivers more flexibility through selective patching and module exclusion.

Final thoughts

The choice between time-machine and freezegun depends on what you value most.

Pick time-machine if you want blazing-fast tests, precise control over time, and you're building something new where performance matters more than backward compatibility.

Choose freezegun if you need a reliable, well-known library that works across many Python versions, or if you're maintaining a project that already uses it.

Both libraries are great at bending time to your will. If speed and precision are your top priorities, time-machine is a clear winner. If you need flexibility and proven stability, freezegun won’t let you down.

Author's avatar
Article by
Stanley Ulili
Stanley Ulili is a technical educator at Better Stack based in Malawi. He specializes in backend development and has freelanced for platforms like DigitalOcean, LogRocket, and AppSignal. Stanley is passionate about making complex topics accessible to developers.
Got an article suggestion? Let us know
Next article
Introduction to Modern Load Testing with Grafana K6
Learn how to use Grafana k6 for load testing your applications. This guide covers installation, writing your first test script, running tests, understanding results, and visualizing performance data.
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github