Last modified: Oct 24, 2026

Time-Travel Tests with freezegun

Testing code that depends on time is hard. A test passes today and fails tomorrow. A token expires at midnight. A subscription renews on the first of the month. These bugs are painful to catch.

freezegun solves this problem. It is a small Python library that lets you freeze time inside your tests. You can jump to any date, any hour, any second. Your code thinks the clock is real. Your test stays deterministic.

This article walks you through everything you need to know. You will install the library, freeze time, travel forward, patch modules, and avoid common mistakes.

Why Time-Dependent Code Breaks Tests

Many functions call datetime.now() or time.time() directly. The result changes on every run. That makes assertions fragile.

Consider a simple function that checks if a trial period has expired. The output depends on the real clock. You cannot write a stable assertion without controlling time.

You could mock the datetime module by hand. But that gets messy fast. freezegun gives you one clean decorator or context manager instead.

Installing freezegun

Install the package with pip. It works well with pytest and unittest.


pip install freezegun

After installation, you can import the freeze_time decorator in your test files. No extra configuration is needed.

Freezing Time with a Decorator

The easiest way to freeze time is the decorator. Pass any date string in ISO format.


from freezegun import freeze_time
from datetime import datetime

@freeze_time("2024-01-15 10:30:00")
def test_now_is_frozen():
    # datetime.now() returns the frozen value
    assert datetime.now() == datetime(2024, 1, 15, 10, 30, 0)
    print(datetime.now())

2024-01-15 10:30:00

Inside the test, the clock never moves. Every call to datetime.now() returns the same value. The test is fully deterministic.

Using freeze_time as a Context Manager

Sometimes you only need frozen time for a small block. Use the context manager form instead.


from freezegun import freeze_time
from datetime import datetime

def test_partial_freeze():
    before = datetime.now()
    with freeze_time("2020-06-01"):
        # Time is frozen only inside this block
        frozen = datetime.now()
        print("Frozen:", frozen)
    after = datetime.now()
    print("Before:", before)
    print("After:", after)

Frozen: 2020-06-01 00:00:00
Before: 2024-03-02 14:12:55.120034
After: 2024-03-02 14:12:55.120101

This is handy when only one part of a test needs a fixed date. The rest of the test keeps running on the real clock.

Traveling Through Time

Real tests often need time to pass. freezegun lets you move the clock forward with tick().


from freezegun import freeze_time
from datetime import datetime, timedelta

@freeze_time("2024-01-01 12:00:00")
def test_time_moves_forward():
    start = datetime.now()
    # Advance the clock by 90 seconds
    with freeze_time("2024-01-01 12:00:00") as frozen:
        frozen.tick(timedelta(seconds=90))
        later = datetime.now()
    print("Start:", start)
    print("Later:", later)
    assert later - start == timedelta(seconds=90)

Start: 2024-01-01 12:00:00
Later: 2024-01-01 12:01:30

This pattern is perfect for testing timeouts, retry delays, and cache expiry. You control every second.

Testing Expiry Logic

Let us test a realistic function. It checks whether a license key has expired.


from datetime import datetime, timedelta
from freezegun import freeze_time

def is_expired(created_at, days=30):
    # Returns True when the license is older than the limit
    return datetime.now() > created_at + timedelta(days=days)

@freeze_time("2024-05-01")
def test_license_valid():
    created = datetime(2024, 4, 20)
    assert is_expired(created) is False

@freeze_time("2024-06-01")
def test_license_expired():
    created = datetime(2024, 4, 20)
    assert is_expired(created) is True

Both tests are clear and stable. They will pass forever, no matter when you run them.

Freezing time.time and Other Clocks

freezegun patches more than datetime. It also controls time.time() and time.monotonic(). This matters when you test performance code or caches.


import time
from freezegun import freeze_time

@freeze_time("2024-02-14 08:00:00")
def test_time_time():
    # time.time() returns the frozen timestamp
    print(time.time())

1707897600.0

If your code uses other libraries to read the clock, you can patch them too. Pass the module name with tick disabled or enabled as needed.

Patching Third-Party Modules

Some libraries import datetime at load time. freezegun may miss those references. Use the extra modules argument to patch them.


from freezegun import freeze_time

@freeze_time("2024-03-10", as_arg=True)
def test_with_extra_modules(frozen):
    # Patch another module that imported datetime early
    frozen.move_to("2024-03-11")
    print("Moved")

This approach keeps third-party code consistent. It avoids the classic "time is frozen here but not there" bug.

Common Pitfalls

Do not freeze time globally. Keep the freeze inside each test. A global freeze hides real timing issues in other tests.

Watch for cached imports. If a module does from datetime import datetime before the freeze, patch that module explicitly. The as_arg option gives you the frozen object for this job.

Avoid asserting on real time. Never compare frozen output to datetime.now() called outside the freeze. The values will differ.

Keep tests fast. Use tick() instead of sleep(). Your suite stays quick and reliable.

Combining freezegun with Other Test Tools

freezegun plays nicely with pytest, unittest, and mock. You can stack decorators in any order. Just remember that the innermost decorator runs first.

If you also test API clients, you may mock HTTP calls alongside frozen time. That keeps the whole test deterministic. For related Python topics, see our guide on calling OpenAI functions from Python to learn more about testing API workflows.

Data pipelines often depend on timestamps too. If you process text data, you can pair frozen time with tokenizing text using NLTK in Python and still keep every test reproducible.

Backend services that validate message timestamps benefit as well. When you produce and consume Kafka messages in Python, frozen time makes ordering and expiry tests stable.

Best Practices Checklist

Use ISO date strings for readability. Freeze only what the test needs. Assert on exact values. Tick the clock to simulate delays. Patch third-party modules when needed.

Keep each test focused on one time scenario. Name tests after the date they simulate, such as test_expiry_on_new_year. This makes failures easy to read.

Conclusion

freezegun turns flaky time-based tests into reliable ones. You freeze the clock, travel forward, and assert with confidence. The decorator and context manager forms cover most cases. The tick() method handles passing time. The extra modules option covers tricky imports.

Start small. Freeze time in one test today. Then expand to expiry logic, retries, and scheduled jobs. Your test suite will thank you, and your future self will too.