that testing our code is an important and important a part of the software program improvement life cycle. That is maybe much more vital after we’re discussing AI and ML programs, the place an inherent uncertainty and hallucinatory factor are probably already baked in from the outset.
And inside that normal testing framework, testing code that behaves otherwise primarily based on the present date or time generally is a actual headache. How do you reliably verify logic that triggers solely at midnight, calculates relative dates (“2 hours in the past”), or handles difficult conditions like leap years or month-ends? Manually mocking Python’s datetime module could be cumbersome and error-prone.
In the event you’ve ever wrestled with this, you’re not alone. However what when you might merely … cease time? And even journey by means of it inside your checks?
That’s exactly what the Freezegun library enables you to do. It’s a sublime answer to a typical testing drawback, but many skilled Python builders have by no means heard of it.
Freezegun permits your Python checks to simulate particular moments in time by mocking the datetime, date, time, and pendulum Python modules. It’s easy to make use of however highly effective for creating deterministic and dependable checks for time-sensitive code.
Why is Freezegun so useful?
- Determinism. That is Freezegun’s main profit. Exams involving time develop into solely predictable. Operating datetime.now() inside a frozen block returns the identical frozen timestamp, eliminating flaky checks brought on by millisecond variations or date rollovers throughout check execution.
- Simplicity. In comparison with manually patching datetime.now or utilizing unittest.mock, Freezegun is usually a lot cleaner and requires much less boilerplate code, particularly when quickly altering the time.
- Time Journey. Simply simulate particular dates and instances — previous, current, or future. That is essential for testing edge instances, comparable to year-end processing, leap seconds, daylight saving time transitions, or just verifying logic tied to particular occasions.
- Relative Time Testing. Check features that calculate relative instances (e.g., “expires in 3 days”) by freezing time and creating timestamps relative to that frozen second.
- Tick Tock. Freezegun permits time to advance (“tick”) from the frozen second inside a check, which is ideal for testing timeouts, durations, or sequences of time-dependent occasions.
Hopefully, I’ve satisfied you that Freezegun may very well be a precious addition to your Python toolbox. Let’s see it in motion by trying by means of some pattern code snippets.
Establishing a dev atmosphere
However earlier than that, let’s arrange a improvement atmosphere to experiment with. I exploit Miniconda for this, however you need to use any device with which you’re acquainted.
I’m a Home windows person, however I typically develop utilizing WSL2 Ubuntu for Home windows, which is what I’ll be doing right here.
All of the code I present ought to work equally effectively underneath Home windows or Unix-like working programs.
# Create and activate a brand new dev atmosphere
#
(base) $ conda create -n freezegun python=3.12 -y
(base) $ conda activate freezegun
Now, we are able to set up the remaining vital libraries.
(freezegun) $ pip set up freezegun jupyter
I’ll be utilizing Jupyter Pocket book to run my code. To observe alongside, sort jupyter pocket book
into your command immediate. It is best to see a jupyter pocket book open in your browser. If that doesn’t occur routinely, you’ll probably see a screenful of knowledge after the jupyter pocket book
command. Close to the underside, you will discover a URL to repeat and paste into your browser to launch the Jupyter Pocket book.
Your URL shall be totally different to mine, nevertheless it ought to look one thing like this:-
http://127.0.0.1:8888/tree?token=3b9f7bd07b6966b41b68e2350721b2d0b6f388d248cc69da
A fast apart: The code I’m displaying in my examples under makes heavy use of the Python assert command. In the event you haven’t come throughout this operate earlier than or haven’t carried out a lot unit testing in Python, assert is used to check if a situation is true, and if it isn’t, it raises an
AssertionError.
This helps catch points throughout improvement and is often used for debugging and validating assumptions within the code.
Instance 1: Fundamental Time Freezing utilizing a Decorator
The commonest means to make use of Freezegun is through its decorator, @freeze_time, which lets you “set” a specific time of day to check numerous time-related features.
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 night!"
# Check the morning greeting
@freeze_time("2023-10-27 09:00:00")
def test_morning_greeting():
print("Operating test_morning_greeting:")
greeting = get_greeting()
print(f" -> Received greeting: '{greeting}'")
assert greeting == "Good morning!"
# Check the night greeting
@freeze_time("2023-10-27 21:30:00")
def test_evening_greeting():
print("nRunning test_evening_greeting:")
greeting = get_greeting()
print(f" -> Received greeting: '{greeting}'")
assert greeting == "Good night!"
# Run the checks
test_morning_greeting()
test_evening_greeting()
print("nBasic decorator checks handed!")
# --- Failure Situation ---
# What occurs if we do not freeze time?
print("n--- Operating with out freeze_time (would possibly fail relying on precise time) ---")
def test_morning_greeting_unfrozen():
print("Operating test_morning_greeting_unfrozen:")
greeting = get_greeting()
print(f" -> Received greeting: '{greeting}'")
# This assertion is now unreliable! It is dependent upon once you run the code.
attempt:
assert greeting == "Good morning!"
print(" (Handed by probability)")
besides AssertionError:
print(" (Failed as anticipated - time wasn't 9 AM)")
test_morning_greeting_unfrozen()
And the output.
Operating test_morning_greeting:
Inside get_greeting(), now = 2023-10-27 09:00:00
-> Received greeting: 'Good morning!'
Operating test_evening_greeting:
Inside get_greeting(), now = 2023-10-27 21:30:00
-> Received greeting: 'Good night!'
Fundamental decorator checks handed!
--- Operating with out freeze_time (would possibly fail relying on precise time) ---
Operating test_morning_greeting_unfrozen:
Inside get_greeting(), now = 2025-04-16 15:00:37.363367
-> Received greeting: 'Good afternoon!'
(Failed as anticipated - time wasn't 9 AM)
Instance 2: Fundamental Time Freezing utilizing a Context Supervisor
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 actuality, time would move
print(f" Inside job: Begin={start_time}, Finish={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" Coming into frozen block at {frozen_time_str}")
begin, finish = process_batch_job()
print(f" Asserting begin == finish: {begin} == {finish}")
assert begin == finish
print(f" Asserting begin == frozen time: {begin} == {datetime.datetime(2023, 11, 15, 10, 0, 0)}")
assert begin == datetime.datetime(2023, 11, 15, 10, 0, 0)
print(" Assertions inside block handed.")
print(" Exited frozen block.")
now_outside = datetime.datetime.now()
print(f" Time exterior block: {now_outside} (needs to be actual time)")
# This assertion simply reveals time is unfrozen, worth is dependent upon actual time
assert now_outside != datetime.datetime(2023, 11, 15, 10, 0, 0)
test_job_timestamps_within_frozen_block()
print("nContext supervisor check handed!")
The output.
Operating test_job_timestamps_within_frozen_block:
Coming into frozen block at 2023-11-15 10:00:00
Inside job: Begin=2023-11-15 10:00:00, Finish=2023-11-15 10:00:00
Asserting begin == finish: 2023-11-15 10:00:00 == 2023-11-15 10:00:00
Asserting begin == frozen time: 2023-11-15 10:00:00 == 2023-11-15 10:00:00
Assertions inside block handed.
Exited frozen block.
Time exterior block: 2025-04-16 15:10:15.231632 (needs to be actual time)
Context supervisor check handed!
Instance 3: Advancing Time with tick
Simulate time passing inside a frozen interval.
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}, Occasion={event_timestamp}, ExpiresAt={event_timestamp + datetime.timedelta(seconds=expiry_duration_seconds)} -> Expired={expired}")
return expired
# --- Handbook ticking utilizing context supervisor ---
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" Occasion created at: {event_time_in_freeze}")
print(" Checking instantly 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 ahead 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(" Handbook tick check completed.")
# --- Failure Situation ---
@freeze_time("2023-10-27 12:00:00") # No tick=True or guide tick
def test_event_expiry_fail_without_tick():
print("n--- Operating test_event_expiry_fail_without_tick (EXPECT ASSERTION ERROR) ---")
event_time = datetime.datetime.now()
expiry_duration = 60
print(f" Occasion created at: {event_time}")
# Simulate work or ready - with out tick, time does not advance!
time.sleep(0.1)
print(f" Time after simulated wait: {datetime.datetime.now()}")
print(" Checking expiry (incorrectly, time did not transfer):")
attempt:
# This could ideally be True, however shall be False with out ticking
assert check_if_event_expired(event_time, expiry_duration)
besides AssertionError:
print(" AssertionError: Occasion didn't expire, as anticipated with out tick.")
print(" Failure state of affairs completed.")
# Run each checks
test_event_expiry_manual_tick()
test_event_expiry_fail_without_tick()
This outputs the next.
Operating test_event_expiry_manual_tick:
Occasion created at: 2023-10-27 12:00:00
Checking instantly after creation:
Checking expiry: Now=2023-10-27 12:00:00, Occasion=2023-10-27 12:00:00, ExpiresAt=2023-10-27 12:01:00 -> Expired=False
Ticking ahead 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, Occasion=2023-10-27 12:00:00, ExpiresAt=2023-10-27 12:01:00 -> Expired=True
Handbook tick check completed.
--- Operating test_event_expiry_fail_without_tick (EXPECT ASSERTION ERROR) ---
Occasion created at: 2023-10-27 12:00:00
Time after simulated wait: 2023-10-27 12:00:00
Checking expiry (incorrectly, time did not transfer):
Checking expiry: Now=2023-10-27 12:00:00, Occasion=2023-10-27 12:00:00, ExpiresAt=2023-10-27 12:01:00 -> Expired=False
AssertionError: Occasion didn't expire, as anticipated with out tick.
Failure state of affairs completed.
Instance 4: Testing Relative Dates
Freezegun ensures steady “time in the past” 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 in the past"
elif delta.seconds >= 3600:
hours = delta.seconds // 3600
rel_time_str = f"{hours} hours in the past"
elif delta.seconds >= 60:
minutes = delta.seconds // 60
rel_time_str = f"{minutes} minutes in the past"
else:
rel_time_str = "simply 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:")
# Occasion occurred 2 days and three hours in the past relative to frozen time
past_event = datetime.datetime(2023, 10, 25, 12, 0, 0)
assert format_relative_time(past_event) == "2 days in the past"
# Occasion occurred 45 minutes in the past
recent_event = datetime.datetime.now() - datetime.timedelta(minutes=45)
assert format_relative_time(recent_event) == "45 minutes in the past"
# Occasion occurred simply now
current_event = datetime.datetime.now() - datetime.timedelta(seconds=10)
assert format_relative_time(current_event) == "simply now"
print(" Relative time checks handed!")
test_relative_time_formatting()
# --- Failure Situation ---
print("n--- Operating relative time with out freeze_time (EXPECT FAILURE) ---")
def test_relative_time_unfrozen():
# Use the identical previous occasion timestamp
past_event = datetime.datetime(2023, 10, 25, 12, 0, 0)
print(f" Testing with past_event = {past_event}")
# This can evaluate in opposition to the *precise* present time, not Oct twenty seventh, 2023
formatted_time = format_relative_time(past_event)
attempt:
assert formatted_time == "2 days in the past"
besides AssertionError:
# The precise distinction shall be a lot bigger!
print(f" AssertionError: Anticipated '2 days in the past', however received '{formatted_time}'. Failed as anticipated.")
test_relative_time_unfrozen()
The output.
Operating test_relative_time_formatting:
Formatting relative time: Now=2023-10-27 15:00:00, Timestamp=2023-10-25 12:00:00 -> '2 days in the past'
Formatting relative time: Now=2023-10-27 15:00:00, Timestamp=2023-10-27 14:15:00 -> '45 minutes in the past'
Formatting relative time: Now=2023-10-27 15:00:00, Timestamp=2023-10-27 14:59:50 -> 'simply now'
Relative time checks handed!
--- Operating relative time with out 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 in the past'
Instance 5: Dealing with Particular Dates (Finish of Month)
Check edge instances, comparable to 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 final day of month: Subsequent day={next_day}, IsLast={is_last}")
return is_last
print("nRunning particular date logic checks:")
@freeze_time("2023-02-28") # Non-leap 12 months
def test_end_of_february_non_leap():
at the moment = datetime.date.at the moment()
assert is_last_day_of_month(at the moment) is True
@freeze_time("2024-02-28") # Bissextile year
def test_end_of_february_leap_not_yet():
at the moment = datetime.date.at the moment()
assert is_last_day_of_month(at the moment) is False # Feb twenty ninth exists
@freeze_time("2024-02-29") # Bissextile year - final day
def test_end_of_february_leap_actual():
at the moment = datetime.date.at the moment()
assert is_last_day_of_month(at the moment) is True
@freeze_time("2023-12-31")
def test_end_of_year():
at the moment = datetime.date.at the moment()
assert is_last_day_of_month(at the moment) 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("Particular date logic checks handed!")
#
# Output
#
Operating particular date logic checks:
Checking if 2023-02-28 is final day of month: Subsequent day=2023-03-01, IsLast=True
Checking if 2024-02-28 is final day of month: Subsequent day=2024-02-29, IsLast=False
Checking if 2024-02-29 is final day of month: Subsequent day=2024-03-01, IsLast=True
Checking if 2023-12-31 is final day of month: Subsequent day=2024-01-01, IsLast=True
pecific date logic checks handed!
Instance 6: Time Zones
Check timezone-aware code accurately, dealing with offsets and transitions like BST/GMT.
# Requires Python 3.9+ for zoneinfo or `pip set up pytz` for older variations
import datetime
from freezegun import freeze_time
attempt:
from zoneinfo import ZoneInfo # Python 3.9+
besides ImportError:
from pytz import timezone as ZoneInfo # Fallback for older Python/pytz
def get_local_and_utc_time():
# Assume native 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 instances: UTC={now_utc}, Native={now_local} ({now_local.tzname()})")
return now_local, now_utc
# Freeze time as 9 AM UTC. London is UTC+1 in summer season (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 checks handed!")
#
# Output
#
Operating test_time_in_london_bst:
Getting instances: UTC=2023-10-27 09:00:00+00:00, Native=2023-10-27 10:00:00+01:00 (BST)
Operating test_time_in_london_gmt:
Getting instances: UTC=2023-12-27 09:00:00+00:00, Native=2023-12-27 09:00:00+00:00 (GMT)
Timezone checks handed!
Instance 7: Specific Time Journey with the move_to operate
Leap between particular time factors in a single check for complicated temporal sequences.
import datetime
from freezegun import freeze_time
class ReportGenerator:
def __init__(self):
self.creation_time = datetime.datetime.now()
self.information = {"standing": "pending", "generated_at": None}
print(f" Report created at {self.creation_time}")
def generate(self):
self.information["status"] = "generated"
self.information["generated_at"] = datetime.datetime.now()
print(f" Report generated at {self.information['generated_at']}")
def get_status_update(self):
now = datetime.datetime.now()
if self.information["status"] == "generated":
time_since_generation = now - self.information["generated_at"]
standing = f"Generated {time_since_generation.seconds} seconds in the past."
else:
time_since_creation = now - self.creation_time
standing = f"Pending for {time_since_creation.seconds} seconds."
print(f" Standing replace at {now}: '{standing}'")
return standing
def test_report_lifecycle():
print("nRunning test_report_lifecycle:")
with freeze_time("2023-11-01 10:00:00") as freezer:
report = ReportGenerator()
assert report.information["status"] == "pending"
# Examine standing after 5 seconds
target_time = datetime.datetime(2023, 11, 1, 10, 0, 5)
print(f" Shifting 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" Shifting time to {target_time} and producing report")
freezer.move_to(target_time)
report.generate()
assert report.information["status"] == "generated"
assert report.get_status_update() == "Generated 0 seconds in the past."
# Examine standing 30 seconds after technology
target_time = datetime.datetime(2023, 11, 1, 10, 1, 30)
print(f" Shifting time to {target_time}")
freezer.move_to(target_time)
assert report.get_status_update() == "Generated 30 seconds in the past."
print(" Complicated lifecycle check handed!")
test_report_lifecycle()
# --- Failure Situation ---
def test_report_lifecycle_fail_forgot_move():
print("n--- Operating lifecycle check (FAIL - forgot move_to) ---")
with freeze_time("2023-11-01 10:00:00") as freezer:
report = ReportGenerator()
assert report.information["status"] == "pending"
# We INTEND to verify standing after 5 seconds, however FORGET to maneuver time
print(f" Checking standing (time continues to be {datetime.datetime.now()})")
# freezer.move_to("2023-11-01 10:00:05") # <-- Forgotten!
attempt:
assert report.get_status_update() == "Pending for five seconds."
besides AssertionError as e:
print(f" AssertionError: {e}. Failed as anticipated.")
test_report_lifecycle_fail_forgot_move()
Right here’s the output.
Operating test_report_lifecycle:
Report created at 2023-11-01 10:00:00
Shifting time to 2023-11-01 10:00:05
Standing replace at 2023-11-01 10:00:05: 'Pending for five seconds.'
Shifting time to 2023-11-01 10:01:00 and producing report
Report generated at 2023-11-01 10:01:00
Standing replace at 2023-11-01 10:01:00: 'Generated 0 seconds in the past.'
Shifting time to 2023-11-01 10:01:30
Standing replace at 2023-11-01 10:01:30: 'Generated 30 seconds in the past.'
Complicated lifecycle check handed!
--- Operating lifecycle check (FAIL - forgot move_to) ---
Report created at 2023-11-01 10:00:00
Checking standing (time continues to be 2023-11-01 10:00:00)
Standing replace at 2023-11-01 10:00:00: 'Pending for 0 seconds.'
AssertionError: . Failed as anticipated.
Abstract
Freezegun is a unbelievable device for any Python developer who wants to check code involving dates and instances. It transforms probably flaky, hard-to-write checks into easy, sturdy, and deterministic ones. By permitting you to freeze, tick, and journey by means of time with ease — and by making it clear when time isn’t managed — it unlocks the flexibility to successfully and reliably check beforehand difficult situations.
As an instance this, I offered a number of examples protecting totally different situations involving date and time testing and confirmed how utilizing Freezegun eliminates lots of the obstacles {that a} conventional testing framework would possibly encounter.
Whereas we’ve lined the core functionalities, you are able to do extra with Freezegun, and I like to recommend trying out its GitHub page.
Briefly, Freezegun is a library you need to know and use in case your code offers with time and it’s worthwhile to check it totally and reliably.