that testing our code is an important and essential a part of the software development life cycle. That is even perhaps more essential once we’re discussing AI and ML systems, where an inherent uncertainty and hallucinatory element are potentially already baked in from the outset.
And inside that general testing framework, testing code that behaves in another way based on the present date or time generally is a real headache. How do you reliably check logic that triggers only at midnight, calculates relative dates (“2 hours ago”), or handles tricky situations like leap years or month-ends? Manually mocking Python’s datetime module might be cumbersome and error-prone.
In the event you’ve ever wrestled with this, you’re not alone. But what if you happen to could simply … stop time? And even travel through it inside your tests?
That’s precisely what the Freezegun library permits you to do. It’s a chic solution to a standard testing problem, yet many experienced Python developers have never heard of it.
Freezegun allows your Python tests to simulate specific moments in time by mocking the datetime, date, time, and pendulum Python modules. It’s easy to make use of but powerful for creating deterministic and reliable tests for time-sensitive code.
Why is Freezegun so helpful?
- Determinism. That is Freezegun’s primary profit. Tests involving time grow to be entirely predictable. Running datetime.now() inside a frozen block returns the identical frozen timestamp, eliminating flaky tests brought on by millisecond differences or date rollovers during test execution.
- Simplicity. In comparison with manually patching datetime.now or using unittest.mock, Freezegun is usually much cleaner and requires less boilerplate code, especially when temporarily changing the time.
- Time Travel. Easily simulate specific dates and times — past, present, or future. That is crucial for testing edge cases, reminiscent of year-end processing, leap seconds, daylight saving time transitions, or just verifying logic tied to specific events.
- Relative Time Testing. Test functions that calculate relative times (e.g., “expires in 3 days”) by freezing time and creating timestamps relative to that frozen moment.
- Tick Tock. Freezegun allows time to advance (“tick”) from the frozen moment inside a test, which is ideal for testing timeouts, durations, or sequences of time-dependent events.
Hopefully, I’ve convinced you that Freezegun could possibly be a precious addition to your Python toolbox. Let’s see it in motion by searching through some sample code snippets.
Establishing a dev environment
But before that, let’s arrange a development environment to experiment with. I take advantage of Miniconda for this, but you need to use any tool with which you’re familiar.
I’m a Windows user, but I often develop using WSL2 Ubuntu for Windows, which is what I’ll be doing here.
All of the code I show should work equally well under Windows or Unix-like operating systems.
# Create and activate a brand new dev environment
#
(base) $ conda create -n freezegun python=3.12 -y
(base) $ conda activate freezegun
Now, we are able to install the remaining mandatory libraries.
(freezegun) $ pip install freezegun jupyter
I’ll be using Jupyter Notebook to run my code. To follow along, type jupyter notebook
into your command prompt. You need to see a jupyter notebook open in your browser. If that doesn’t occur mechanically, you’ll likely see a screenful of data after the jupyter notebook
command. Near the underside, you can see a URL to repeat and paste into your browser to launch the Jupyter Notebook.
Your URL will probably be different to mine, nevertheless it should look something like this:-
http://127.0.0.1:8888/tree?token=3b9f7bd07b6966b41b68e2350721b2d0b6f388d248cc69da
A fast aside: The code I’m showing in my examples below makes heavy use of the Python assert command. In the event you haven’t come across this function before or haven’t done much unit testing in Python, assert is used to check if a condition is true, and if it isn’t, it raises an
AssertionError.
This helps catch issues during development and is often used for debugging and validating assumptions within the code.
Example 1: Basic Time Freezing using a Decorator
Essentially the most common solution to use Freezegun is via its decorator, @freeze_time, which lets you “set” a selected time of day to check various time-related functions.
import datetime
from freezegun import freeze_time
def get_greeting():
now = datetime.datetime.now()
print(f" Inside get_greeting(), now = {now}") # Added print
if now.hour < 12:
return "Good morning!"
elif 12 <= now.hour < 18:
return "Good afternoon!"
else:
return "Good evening!"
# Test the morning greeting
@freeze_time("2023-10-27 09:00:00")
def test_morning_greeting():
print("Running test_morning_greeting:")
greeting = get_greeting()
print(f" -> Got greeting: '{greeting}'")
assert greeting == "Good morning!"
# Test the evening greeting
@freeze_time("2023-10-27 21:30:00")
def test_evening_greeting():
print("nRunning test_evening_greeting:")
greeting = get_greeting()
print(f" -> Got greeting: '{greeting}'")
assert greeting == "Good evening!"
# Run the tests
test_morning_greeting()
test_evening_greeting()
print("nBasic decorator tests passed!")
# --- Failure Scenario ---
# What happens if we do not freeze time?
print("n--- Running without freeze_time (might fail depending on actual time) ---")
def test_morning_greeting_unfrozen():
print("Running test_morning_greeting_unfrozen:")
greeting = get_greeting()
print(f" -> Got greeting: '{greeting}'")
# This assertion is now unreliable! It depends upon while you run the code.
try:
assert greeting == "Good morning!"
print(" (Passed by likelihood)")
except AssertionError:
print(" (Failed as expected - time wasn't 9 AM)")
test_morning_greeting_unfrozen()
And the output.
Running test_morning_greeting:
Inside get_greeting(), now = 2023-10-27 09:00:00
-> Got greeting: 'Good morning!'
Running test_evening_greeting:
Inside get_greeting(), now = 2023-10-27 21:30:00
-> Got greeting: 'Good evening!'
Basic decorator tests passed!
--- Running without freeze_time (might fail depending on actual time) ---
Running test_morning_greeting_unfrozen:
Inside get_greeting(), now = 2025-04-16 15:00:37.363367
-> Got greeting: 'Good afternoon!'
(Failed as expected - time wasn't 9 AM)
Example 2: Basic Time Freezing using a Context Manager
Create a “block” of frozen time.
import datetime
from freezegun import freeze_time
def process_batch_job():
start_time = datetime.datetime.now()
# Simulate work
end_time = datetime.datetime.now() # In point of fact, time would pass
print(f" Inside job: Start={start_time}, End={end_time}") # Added print
return (start_time, end_time)
def test_job_timestamps_within_frozen_block():
print("nRunning test_job_timestamps_within_frozen_block:")
frozen_time_str = "2023-11-15 10:00:00"
with freeze_time(frozen_time_str):
print(f" Entering frozen block at {frozen_time_str}")
start, end = process_batch_job()
print(f" Asserting start == end: {start} == {end}")
assert start == end
print(f" Asserting start == frozen time: {start} == {datetime.datetime(2023, 11, 15, 10, 0, 0)}")
assert start == datetime.datetime(2023, 11, 15, 10, 0, 0)
print(" Assertions inside block passed.")
print(" Exited frozen block.")
now_outside = datetime.datetime.now()
print(f" Time outside block: {now_outside} (needs to be real time)")
# This assertion just shows time is unfrozen, value depends upon real time
assert now_outside != datetime.datetime(2023, 11, 15, 10, 0, 0)
test_job_timestamps_within_frozen_block()
print("nContext manager test passed!")
The output.
Running test_job_timestamps_within_frozen_block:
Entering frozen block at 2023-11-15 10:00:00
Inside job: Start=2023-11-15 10:00:00, End=2023-11-15 10:00:00
Asserting start == end: 2023-11-15 10:00:00 == 2023-11-15 10:00:00
Asserting start == frozen time: 2023-11-15 10:00:00 == 2023-11-15 10:00:00
Assertions inside block passed.
Exited frozen block.
Time outside block: 2025-04-16 15:10:15.231632 (needs to be real time)
Context manager test passed!
Example 3: Advancing Time with tick
Simulate time passing inside a frozen period.
import datetime
import time
from freezegun import freeze_time
def check_if_event_expired(event_timestamp, expiry_duration_seconds):
now = datetime.datetime.now()
expired = now > event_timestamp + datetime.timedelta(seconds=expiry_duration_seconds)
print(f" Checking expiry: Now={now}, Event={event_timestamp}, ExpiresAt={event_timestamp + datetime.timedelta(seconds=expiry_duration_seconds)} -> Expired={expired}")
return expired
# --- Manual ticking using context manager ---
def test_event_expiry_manual_tick():
print("nRunning test_event_expiry_manual_tick:")
with freeze_time("2023-10-27 12:00:00") as freezer:
event_time_in_freeze = datetime.datetime.now()
expiry_duration = 60
print(f" Event created at: {event_time_in_freeze}")
print(" Checking immediately after creation:")
assert not check_if_event_expired(event_time_in_freeze, expiry_duration)
# Advance time by 61 seconds
delta_to_tick = datetime.timedelta(seconds=61)
print(f" Ticking forward by {delta_to_tick}...")
freezer.tick(delta=delta_to_tick)
print(f" Time after ticking: {datetime.datetime.now()}")
print(" Checking after ticking:")
assert check_if_event_expired(event_time_in_freeze, expiry_duration)
print(" Manual tick test finished.")
# --- Failure Scenario ---
@freeze_time("2023-10-27 12:00:00") # No tick=True or manual tick
def test_event_expiry_fail_without_tick():
print("n--- Running test_event_expiry_fail_without_tick (EXPECT ASSERTION ERROR) ---")
event_time = datetime.datetime.now()
expiry_duration = 60
print(f" Event created at: {event_time}")
# Simulate work or waiting - without tick, time doesn't advance!
time.sleep(0.1)
print(f" Time after simulated wait: {datetime.datetime.now()}")
print(" Checking expiry (incorrectly, time didn't move):")
try:
# This could ideally be True, but will probably be False without ticking
assert check_if_event_expired(event_time, expiry_duration)
except AssertionError:
print(" AssertionError: Event didn't expire, as expected without tick.")
print(" Failure scenario finished.")
# Run each tests
test_event_expiry_manual_tick()
test_event_expiry_fail_without_tick()
This outputs the next.
Running test_event_expiry_manual_tick:
Event created at: 2023-10-27 12:00:00
Checking immediately after creation:
Checking expiry: Now=2023-10-27 12:00:00, Event=2023-10-27 12:00:00, ExpiresAt=2023-10-27 12:01:00 -> Expired=False
Ticking forward by 0:01:01...
Time after ticking: 2023-10-27 12:01:01
Checking after ticking:
Checking expiry: Now=2023-10-27 12:01:01, Event=2023-10-27 12:00:00, ExpiresAt=2023-10-27 12:01:00 -> Expired=True
Manual tick test finished.
--- Running test_event_expiry_fail_without_tick (EXPECT ASSERTION ERROR) ---
Event created at: 2023-10-27 12:00:00
Time after simulated wait: 2023-10-27 12:00:00
Checking expiry (incorrectly, time didn't move):
Checking expiry: Now=2023-10-27 12:00:00, Event=2023-10-27 12:00:00, ExpiresAt=2023-10-27 12:01:00 -> Expired=False
AssertionError: Event didn't expire, as expected without tick.
Failure scenario finished.
Example 4: Testing Relative Dates
Freezegun ensures stable “time ago” logic.
import datetime
from freezegun import freeze_time
def format_relative_time(timestamp):
now = datetime.datetime.now()
delta = now - timestamp
rel_time_str = ""
if delta.days > 0:
rel_time_str = f"{delta.days} days ago"
elif delta.seconds >= 3600:
hours = delta.seconds // 3600
rel_time_str = f"{hours} hours ago"
elif delta.seconds >= 60:
minutes = delta.seconds // 60
rel_time_str = f"{minutes} minutes ago"
else:
rel_time_str = "just now"
print(f" Formatting relative time: Now={now}, Timestamp={timestamp} -> '{rel_time_str}'")
return rel_time_str
@freeze_time("2023-10-27 15:00:00")
def test_relative_time_formatting():
print("nRunning test_relative_time_formatting:")
# Event happened 2 days and three hours ago relative to frozen time
past_event = datetime.datetime(2023, 10, 25, 12, 0, 0)
assert format_relative_time(past_event) == "2 days ago"
# Event happened 45 minutes ago
recent_event = datetime.datetime.now() - datetime.timedelta(minutes=45)
assert format_relative_time(recent_event) == "45 minutes ago"
# Event happened just now
current_event = datetime.datetime.now() - datetime.timedelta(seconds=10)
assert format_relative_time(current_event) == "just now"
print(" Relative time tests passed!")
test_relative_time_formatting()
# --- Failure Scenario ---
print("n--- Running relative time without freeze_time (EXPECT FAILURE) ---")
def test_relative_time_unfrozen():
# Use the identical past event timestamp
past_event = datetime.datetime(2023, 10, 25, 12, 0, 0)
print(f" Testing with past_event = {past_event}")
# This can compare against the *actual* current time, not Oct twenty seventh, 2023
formatted_time = format_relative_time(past_event)
try:
assert formatted_time == "2 days ago"
except AssertionError:
# The actual difference will probably be much larger!
print(f" AssertionError: Expected '2 days ago', but got '{formatted_time}'. Failed as expected.")
test_relative_time_unfrozen()
The output.
Running test_relative_time_formatting:
Formatting relative time: Now=2023-10-27 15:00:00, Timestamp=2023-10-25 12:00:00 -> '2 days ago'
Formatting relative time: Now=2023-10-27 15:00:00, Timestamp=2023-10-27 14:15:00 -> '45 minutes ago'
Formatting relative time: Now=2023-10-27 15:00:00, Timestamp=2023-10-27 14:59:50 -> 'just now'
Relative time tests passed!
--- Running relative time without freeze_time (EXPECT FAILURE) ---
Testing with past_event = 2023-10-25 12:00:00
Formatting relative time: Now=2023-10-27 12:00:00, Timestamp=2023-10-25 12:00:00 -> '2 days ago'
Example 5: Handling Specific Dates (End of Month)
Test edge cases, reminiscent of leap years, reliably.
import datetime
from freezegun import freeze_time
def is_last_day_of_month(check_date):
next_day = check_date + datetime.timedelta(days=1)
is_last = next_day.month != check_date.month
print(f" Checking if {check_date} is last day of month: Next day={next_day}, IsLast={is_last}")
return is_last
print("nRunning specific date logic tests:")
@freeze_time("2023-02-28") # Non-leap yr
def test_end_of_february_non_leap():
today = datetime.date.today()
assert is_last_day_of_month(today) is True
@freeze_time("2024-02-28") # Intercalary year
def test_end_of_february_leap_not_yet():
today = datetime.date.today()
assert is_last_day_of_month(today) is False # Feb twenty ninth exists
@freeze_time("2024-02-29") # Intercalary year - last day
def test_end_of_february_leap_actual():
today = datetime.date.today()
assert is_last_day_of_month(today) is True
@freeze_time("2023-12-31")
def test_end_of_year():
today = datetime.date.today()
assert is_last_day_of_month(today) is True
test_end_of_february_non_leap()
test_end_of_february_leap_not_yet()
test_end_of_february_leap_actual()
test_end_of_year()
print("Specific date logic tests passed!")
#
# Output
#
Running specific date logic tests:
Checking if 2023-02-28 is last day of month: Next day=2023-03-01, IsLast=True
Checking if 2024-02-28 is last day of month: Next day=2024-02-29, IsLast=False
Checking if 2024-02-29 is last day of month: Next day=2024-03-01, IsLast=True
Checking if 2023-12-31 is last day of month: Next day=2024-01-01, IsLast=True
pecific date logic tests passed!
Example 6: Time Zones
Test timezone-aware code accurately, handling offsets and transitions like BST/GMT.
# Requires Python 3.9+ for zoneinfo or `pip install pytz` for older versions
import datetime
from freezegun import freeze_time
try:
from zoneinfo import ZoneInfo # Python 3.9+
except ImportError:
from pytz import timezone as ZoneInfo # Fallback for older Python/pytz
def get_local_and_utc_time():
# Assume local timezone is Europe/London for this instance
local_tz = ZoneInfo("Europe/London")
now_utc = datetime.datetime.now(datetime.timezone.utc)
now_local = now_utc.astimezone(local_tz)
print(f" Getting times: UTC={now_utc}, Local={now_local} ({now_local.tzname()})")
return now_local, now_utc
# Freeze time as 9 AM UTC. London is UTC+1 in summer (BST). Oct 27 is BST.
@freeze_time("2023-10-27 09:00:00", tz_offset=0) # tz_offset=0 means the frozen time string IS UTC
def test_time_in_london_bst():
print("nRunning test_time_in_london_bst:")
local_time, utc_time = get_local_and_utc_time()
assert utc_time.hour == 9
assert local_time.hour == 10 # London is UTC+1 on this date
assert local_time.tzname() == "BST"
# Freeze time as 9 AM UTC. Use December twenty seventh, which is GMT (UTC+0)
@freeze_time("2023-12-27 09:00:00", tz_offset=0)
def test_time_in_london_gmt():
print("nRunning test_time_in_london_gmt:")
local_time, utc_time = get_local_and_utc_time()
assert utc_time.hour == 9
assert local_time.hour == 9 # London is UTC+0 on this date
assert local_time.tzname() == "GMT"
test_time_in_london_bst()
test_time_in_london_gmt()
print("nTimezone tests passed!")
#
# Output
#
Running test_time_in_london_bst:
Getting times: UTC=2023-10-27 09:00:00+00:00, Local=2023-10-27 10:00:00+01:00 (BST)
Running test_time_in_london_gmt:
Getting times: UTC=2023-12-27 09:00:00+00:00, Local=2023-12-27 09:00:00+00:00 (GMT)
Timezone tests passed!
Example 7: Explicit Time Travel with the move_to function
Jump between specific time points in a single test for complex temporal sequences.
import datetime
from freezegun import freeze_time
class ReportGenerator:
def __init__(self):
self.creation_time = datetime.datetime.now()
self.data = {"status": "pending", "generated_at": None}
print(f" Report created at {self.creation_time}")
def generate(self):
self.data["status"] = "generated"
self.data["generated_at"] = datetime.datetime.now()
print(f" Report generated at {self.data['generated_at']}")
def get_status_update(self):
now = datetime.datetime.now()
if self.data["status"] == "generated":
time_since_generation = now - self.data["generated_at"]
status = f"Generated {time_since_generation.seconds} seconds ago."
else:
time_since_creation = now - self.creation_time
status = f"Pending for {time_since_creation.seconds} seconds."
print(f" Status update at {now}: '{status}'")
return status
def test_report_lifecycle():
print("nRunning test_report_lifecycle:")
with freeze_time("2023-11-01 10:00:00") as freezer:
report = ReportGenerator()
assert report.data["status"] == "pending"
# Check status after 5 seconds
target_time = datetime.datetime(2023, 11, 1, 10, 0, 5)
print(f" Moving time to {target_time}")
freezer.move_to(target_time)
assert report.get_status_update() == "Pending for five seconds."
# Generate the report at 10:01:00
target_time = datetime.datetime(2023, 11, 1, 10, 1, 0)
print(f" Moving time to {target_time} and generating report")
freezer.move_to(target_time)
report.generate()
assert report.data["status"] == "generated"
assert report.get_status_update() == "Generated 0 seconds ago."
# Check status 30 seconds after generation
target_time = datetime.datetime(2023, 11, 1, 10, 1, 30)
print(f" Moving time to {target_time}")
freezer.move_to(target_time)
assert report.get_status_update() == "Generated 30 seconds ago."
print(" Complex lifecycle test passed!")
test_report_lifecycle()
# --- Failure Scenario ---
def test_report_lifecycle_fail_forgot_move():
print("n--- Running lifecycle test (FAIL - forgot move_to) ---")
with freeze_time("2023-11-01 10:00:00") as freezer:
report = ReportGenerator()
assert report.data["status"] == "pending"
# We INTEND to envision status after 5 seconds, but FORGET to maneuver time
print(f" Checking status (time continues to be {datetime.datetime.now()})")
# freezer.move_to("2023-11-01 10:00:05") # <-- Forgotten!
try:
assert report.get_status_update() == "Pending for five seconds."
except AssertionError as e:
print(f" AssertionError: {e}. Failed as expected.")
test_report_lifecycle_fail_forgot_move()
Here’s the output.
Running test_report_lifecycle:
Report created at 2023-11-01 10:00:00
Moving time to 2023-11-01 10:00:05
Status update at 2023-11-01 10:00:05: 'Pending for five seconds.'
Moving time to 2023-11-01 10:01:00 and generating report
Report generated at 2023-11-01 10:01:00
Status update at 2023-11-01 10:01:00: 'Generated 0 seconds ago.'
Moving time to 2023-11-01 10:01:30
Status update at 2023-11-01 10:01:30: 'Generated 30 seconds ago.'
Complex lifecycle test passed!
--- Running lifecycle test (FAIL - forgot move_to) ---
Report created at 2023-11-01 10:00:00
Checking status (time continues to be 2023-11-01 10:00:00)
Status update at 2023-11-01 10:00:00: 'Pending for 0 seconds.'
AssertionError: . Failed as expected.
Summary
Freezegun is a improbable tool for any Python developer who needs to check code involving dates and times. It transforms potentially flaky, hard-to-write tests into easy, robust, and deterministic ones. By allowing you to freeze, tick, and travel through time with ease — and by making it clear when time controlled — it unlocks the flexibility to effectively and reliably test previously difficult scenarios.
As an example this, I provided several examples covering different instances involving date and time testing and showed how using Freezegun eliminates lots of the obstacles that a conventional testing framework might encounter.
While we’ve covered the core functionalities, you possibly can do more with Freezegun, and I like to recommend testing its GitHub page.
In brief, Freezegun is a library you need to know and use in case your code deals with time and you might want to test it thoroughly and reliably.