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
?
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
?
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:
- Direct interception of time calls at the C level
- Elimination of Python-level function wrapping overhead
- 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.
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
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.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github