Mastering Time: Tips and Tricks for Faking Time
Exploring localization with Typer, Freezegun, Libfaketime, and time-machine
Our systems are bound to time: when the action or event happened, when the system became aware of it, and when it reacted to it.
Therefore, faking time is an essential functionality we use in our day-to-day life in software/data engineering. In this article, we will go through three ways of faking time while using Typer as well — the easiest command line interface builder.
Let’s suppose we have an application that returns a message mentioning if a certain date is the last day of a month or not. The following implementation is available on GitHub.
What’s the Last Day of the Month
We first implement a function that inputs a datetime parameter and uses structural pattern matching in Python to determine if the datetime is the last day of the month or not based on the year. For instance, it looks at whether it is a leap year, February, an odd or even month, and the day (28 or 29, 30 or 31). Here’s the code:
def is_last_day_of_month(input_datetime: date) -> bool:
"""
:param input_datetime: date object
:return:
True, if datetime is last day of month
False, otherwise
"""
date_year = input_datetime.year
date_month = input_datetime.month
date_day = input_datetime.day
match date_month:
case odd_month if odd_month in [1, 3, 5, 7, 8, 10, 12]:
match date_day:
case 31:
return True
case _:
return False
case even_month if even_month in [4, 6, 9, 11]:
match date_day:
case 30:
return True
case _:
return False
case 2:
match date_year % 4:
case 0:
match date_day:
case 29:
return True
case _:
return False
case _:
match date_day:
case 28:
return True
case _:
return False
case _:
raise ValueError(f"Unexpected values for {input_datetime} {date_year} {date_month} {date_day}")For the above function, we define a test that we can execute for one year's worth of dates and verify our function returns the correct response. We will use the pytest parametrize, so we can execute the same test with a different input. Here’s what that looks like:
import datetime
import pytest
from faking_time_demo.main import is_last_day_of_month
start_date = datetime.date(2023, 1, 1)
test_dates = [start_date + datetime.timedelta(days=no_days) for no_days in range(365)]
test_scenario_bool = [
(
date_,
True
if date_
in [
datetime.date(2023, 1, 31),
datetime.date(2023, 2, 28),
datetime.date(2023, 3, 31),
datetime.date(2023, 4, 30),
datetime.date(2023, 5, 31),
datetime.date(2023, 6, 30),
datetime.date(2023, 7, 31),
datetime.date(2023, 8, 31),
datetime.date(2023, 9, 30),
datetime.date(2023, 10, 31),
datetime.date(2023, 11, 30),
datetime.date(2023, 12, 31),
]
else False,
)
for date_ in test_dates
]
test_scenario_bool.append((datetime.date(2024, 2, 29), True))
@pytest.mark.parametrize(("input_datetime", "expected_result"), test_scenario_bool)
def test_is_last_day_of_month(input_datetime, expected_result):
with freeze_time(input_datetime):
assert is_last_day_of_month(input_datetime=input_datetime) is expected_resultIn the above code, we created a list with 366 dates (2023 plus 29-02-2024 to test the leap year). With parametrize, we execute the same test for each item from the list. Here’s the code:
(faking-time-demo-py3.11) acirtep ~/faking-time-demo [main] $ pytest tests/test_last_day.py::test_get_text_for_last_day
================================================================================ test session starts ================================================================================
platform darwin -- Python 3.11.0, pytest-7.2.2, pluggy-1.0.0
collected 366 items
tests/test_last_day.py ...................................................................................................................................................... [ 40%]
............................................................................................................................................................................. [ 88%]
........................................... [100%]
================================================================================ 366 passed in 0.92s ================================================================================Tell Me if It’s the Last Day of the Month
Now that we have a function that checks if a datetime is the last day of the month, we can use it to print a message. Here’s how to do that:
def get_text_for_day(input_timezone: AvailableTimeZone = None):
timezone_to_set = input_timezone or time.tzname[0]
current_datetime = datetime.now(tz=pytz.timezone(timezone_to_set))
is_last_day = is_last_day_of_month(current_datetime)
typer.echo(typer.style(f"{current_datetime} is expressed in {timezone_to_set}"))
if is_last_day:
message = f"{current_datetime} is last day of the month"
typer.echo(typer.style(message, fg=typer.colors.GREEN))
else:
message = f"{current_datetime} is NOT last day of the month"
typer.echo(typer.style(message, fg=typer.colors.RED))
return message
if __name__ == "__main__":
typer.run(get_text_for_day)In the above code, we implemented a typer CLI with the following definition:
(faking-time-demo-py3.11) acirtep ~/faking-time-demo [main] $python ./faking_time_demo/main.py --help
Usage: main.py [OPTIONS]
Options:
--input-timezone [UTC|Europe/Amsterdam|Europe/Bucharest|US/Eastern]
[default: AvailableTimeZone.UTC]
--help Show this message and exit.And here, we used an Enum called AvailableTimeZone as an argument that accepts the following four time zones:
from enum import Enum
class AvailableTimeZone(str, Enum):
UTC = "UTC"
NETHERLANDS = "Europe/Amsterdam"
ROMANIA = "Europe/Bucharest"
NEW_YORK = "US/Eastern"The above command will print a message, in either green or red, if the current datetime is the last day of the month or not.
(faking-time-demo-py3.11) acirtep ~/faking-time-demo [main] $ python ./faking_time_demo/main.py --input-timezone UTC
2023-03-27 15:38:26.323668+00:00 is NOT last day of the monthFreezing Time, the Secret Weapon
While testing the function is_last_day_of_month was straightforward because the datetime was an input parameter, testing the get_text_for_day is different because it uses datetime.now().
def test_get_text_for_day():
message = get_text_for_day(input_timezone=AvailableTimeZone.UTC)
assert "is NOT last day" in messageWhile writing the above definition, I knew the date I executed the test on wasn’t the last day, but will that be the case for future executions? Nope. This is a flaky test that will fail at any month’s end.
And so, freezegun comes to the rescue by freezing the time based on our input.
First, we need to define a set of scenarios to test the result against. Here’s how to do that:
test_scenario_message = [
(
date_,
"is last day of the month"
if date_
in [
datetime.date(2023, 1, 31),
datetime.date(2023, 2, 28),
datetime.date(2023, 3, 31),
datetime.date(2023, 4, 30),
datetime.date(2023, 5, 31),
datetime.date(2023, 6, 30),
datetime.date(2023, 7, 31),
datetime.date(2023, 8, 31),
datetime.date(2023, 9, 30),
datetime.date(2023, 10, 31),
datetime.date(2023, 11, 30),
datetime.date(2023, 12, 31),
]
else "is NOT last day of the month",
)
for date_ in test_dates
]
test_scenario_message.append((datetime.date(2024, 2, 29), "is last day of the month"))With the above test data and freeze_time, we can run the following test and expect consistency between runs:
import pytest
from freezegun import freeze_time
from faking_time_demo.constants import AvailableTimeZone
from faking_time_demo.main import get_text_for_day
@pytest.mark.parametrize(("input_datetime", "expected_result"), test_scenario_message)
def test_get_text_for_day(input_datetime, expected_result):
with freeze_time(input_datetime):
message = get_text_for_day(input_timezone=AvailableTimeZone.UTC)
assert expected_result in messageIn the code above, freeze_time will mock datetime with the date provided. Therefore, whenever we execute get_text_for_day, datetime.now() will be executed with the date that’s tied to the input given to freeze_time.

What’s a Date Without a Time Zone?
While time zones provoke negative reactions even to most senior engineers, they are important irrespective of how global your application is. Whenever we work with datetime, we need to be aware of the following:
- the time zone of the system our application runs on
- the time zone of the database
- the time zone of the user
Let’s experiment with the above CLI on two Docker containers that have a different time zone. These are configured with the TZ env variable in docker-compose. For each value configured in AvailableTimeZone, a service is created as shown:
services:
faking_time_utc:
container_name: faking_time_utc
restart: on-failure
build:
context: .
dockerfile: Dockerfile
environment:
- PYTHONPATH=/app
- TZ=UTC
volumes:
- ./faking_time_demo:/app/faking_time_demo
- ./tests:/app/tests
command:
- bash
- -c
- tail -f /dev/null
...Execute docker-compose up and four containers that are available for test. Here’s the code:
$ docker exec -it faking_time_est python /app/faking_time_demo/main.py
2023-03-27 14:37:34.357152-05:00 is expressed in EST
2023-03-27 14:37:34.357152-05:00 is NOT last day of the month
$ docker exec -it faking_time_utc python /app/faking_time_demo/main.py
2023-03-27 19:37:43.613762+00:00 is expressed in UTC
2023-03-27 19:37:43.613762+00:00 is NOT last day of the month
$ docker exec -it faking_time_ro python /app/faking_time_demo/main.py
2023-03-27 22:40:05.918796+03:00 is expressed in EET
2023-03-27 22:40:05.918796+03:00 is NOT last day of the month
$ docker exec -it faking_time_nl python /app/faking_time_demo/main.py
2023-03-27 21:40:13.214218+02:00 is expressed in CET
2023-03-27 21:40:13.214218+02:00 is NOT last day of the monthWhile checking the test for the get_text function, we observe it is always checking for the UTC time zone:
@pytest.mark.parametrize(("input_datetime", "expected_result"), test_scenario_message)
def test_get_text_for_day(input_datetime, expected_result):
with freeze_time(input_datetime):
message = get_text_for_day(input_timezone=AvailableTimeZone.UTC)
assert expected_result in messageThis happens because freeze_time’s default datetime is UTC. If we remove the input_timezone default value and execute pytest on each container, we can see that depending on the container’s time zone, the tests succeeded for UTC, CET, and EET and failed for EST.
$ docker exec -it faking_time_nl pytest -v -rP /app/tests/test_last_day.py::test_get_text_for_day
================================================================================================================================================= test session starts ==================================================================================================================================================
2023-01-01 01:00:00+01:00 is expressed in CET
2023-01-01 01:00:00+01:00 is NOT last day of the month
================================================================================================================================================== 1 passed in 0.08s ===================================================================================================================================================
$ docker exec -it faking_time_ro pytest -v -rP /app/tests/test_last_day.py::test_get_text_for_day
================================================================================================================================================= test session starts ==================================================================================================================================================
2023-01-01 02:00:00+02:00 is expressed in EET
2023-01-01 02:00:00+02:00 is NOT last day of the month
================================================================================================================================================== 1 passed in 0.07s ===================================================================================================================================================
$ docker exec -it faking_time_utc pytest -v -rP /app/tests/test_last_day.py::test_get_text_for_day
================================================================================================================================================= test session starts ==================================================================================================================================================
2023-01-01 00:00:00+00:00 is expressed in UTC
2023-01-01 00:00:00+00:00 is NOT last day of the month
================================================================================================================================================== 1 passed in 0.09s ===================================================================================================================================================
$ docker exec -it faking_time_est pytest -v -rP /app/tests/test_last_day.py::test_get_text_for_day
================================================================================================================================================= test session starts ==================================================================================================================================================
platform linux -- Python 3.11.0, pytest-7.2.2, pluggy-1.0.0 -- /usr/local/bin/python
cachedir: .pytest_cache
rootdir: /app
collected 1 item
tests/test_last_day.py::test_get_text_for_day[input_datetime0-is NOT last day of the month] FAILED [100%]
======================================================================================================================================================= FAILURES =======================================================================================================================================================
_________________________________________________________________________________________________________________________ test_get_text_for_day[input_datetime0-is NOT last day of the month] __________________________________________________________________________________________________________________________
input_datetime = datetime.date(2023, 1, 1), expected_result = 'is NOT last day of the month'
@pytest.mark.parametrize(("input_datetime", "expected_result"), test_scenario_message[0:1])
def test_get_text_for_day(input_datetime, expected_result):
with freeze_time(input_datetime):
message = get_text_for_day()
> assert expected_result in message
E AssertionError: assert 'is NOT last day of the month' in '2022-12-31 19:00:00-05:00 is last day of the month'
tests/test_last_day.py:87: AssertionError
------------------------------------------------------------------------------------------------------------------------------------------------- Captured stdout call -------------------------------------------------------------------------------------------------------------------------------------------------
2022-12-31 19:00:00-05:00 is expressed in EST
2022-12-31 19:00:00-05:00 is last day of the month
================================================================================================================================================== 1 failed in 0.09s ===================================================================================================================================================To fix the above error, we need to configure the offset for freeze_time. Here’s how to do that:
def get_offset(tzname):
match tzname:
case "UTC":
return 0
case "EST":
return 5
case "CET":
return -1
case "EET":
return -2
case _:
raise Exception("Unknown timezone")
@pytest.mark.parametrize(("input_datetime", "expected_result"), test_scenario_message[0:1])
def test_get_text_for_day(input_datetime, expected_result):
with freeze_time(input_datetime, tz_offset=get_offset(time.tzname[0])):
message = get_text_for_day()
assert expected_result in messageWe mock the system's timestamp with the code above by assigning the offset compared to the UTC. After re-executing the tests, we get the following:
$ docker exec -it faking_time_nl pytest -v -rP /app/tests/test_last_day.py::test_get_text_for_day
================================================================================================================================================= test session starts ==================================================================================================================================================
2023-01-01 00:00:00+01:00 is expressed in CET
2023-01-01 00:00:00+01:00 is NOT last day of the month
================================================================================================================================================== 1 passed in 0.06s ===================================================================================================================================================
$ docker exec -it faking_time_ro pytest -v -rP /app/tests/test_last_day.py::test_get_text_for_day
================================================================================================================================================= test session starts ==================================================================================================================================================
2023-01-01 00:00:00+02:00 is expressed in EET
2023-01-01 00:00:00+02:00 is NOT last day of the month
================================================================================================================================================== 1 passed in 0.06s ===================================================================================================================================================
$ docker exec -it faking_time_utc pytest -v -rP /app/tests/test_last_day.py::test_get_text_for_day
================================================================================================================================================= test session starts ==================================================================================================================================================
2023-01-01 00:00:00+00:00 is expressed in UTC
2023-01-01 00:00:00+00:00 is NOT last day of the month
================================================================================================================================================== 1 passed in 0.06s ===================================================================================================================================================
$ docker exec -it faking_time_est pytest -v -rP /app/tests/test_last_day.py::test_get_text_for_day
================================================================================================================================================= test session starts ==================================================================================================================================================
2023-01-01 00:00:00-05:00 is expressed in EST
2023-01-01 00:00:00-05:00 is NOT last day of the month
================================================================================================================================================== 1 passed in 0.06s ===================================================================================================================================================Faking System Time
While freezegun is a powerful library to manipulate time for testing purposes, there might be a case in which one needs to manipulate the system time during execution too. To do this, libfaketime can be installed in the docker image using this command:
RUN apt-get update && \
apt-get -y install libpq-dev python3-dev gcc libfaketimeWe can now execute the CLI to print the message inside the docker container by providing a fake time as the system time. Here’s what that looks like:
root@c7e6aad858cc:/app# LD_PRELOAD=/usr/lib/aarch64-linux-gnu/faketime/libfaketime.so.1 FAKETIME="2023-09-29 22:00:00" python /app/faking_time_demo/main.py --input-timezone Europe/Amsterdam
2023-09-30 00:00:00+02:00 is expressed in AvailableTimeZone.NETHERLANDS
2023-09-30 00:00:00+02:00 is last day of the monthHere’s the results for the rest of the time zones:

Hmm, wait a minute! With freeze time, the UTC offset for Amsterdam is +1, and now with libfaketime, it is +2. That is because we must also be aware of daylight saving time. If you are interested in understanding UTC offset, I recommend getting started with the Wikipedia page.
An Alternative to Freezegun
While having some issues with testing UTC datetime with freezegun, I’ve discovered time-machine. Instead of using freeze_time, we can use time_machine.travel:
import time_machine
@pytest.mark.parametrize(("input_datetime", "expected_result"), test_scenario_message)
def test_get_text_for_day_time_machine(input_datetime, expected_result):
input_datetime = input_datetime.replace(tzinfo=timezone(time.tzname[0]))
with time_machine.travel(input_datetime):
message = get_text_for_day()
assert expected_result in messageThe benefit of time-machine is that it is faster than freezegun, as the author describes here. The speed is easily observed in our demo tests as well: the tests with time_machine are executed in 0.23 seconds, while the ones with freezegun in 1.06 seconds.
$ docker exec -it faking_time_est pytest /app/tests/test_last_day.py::test_get_text_for_day_freeze_gun
================================================================================================================================================= test session starts ==================================================================================================================================================
platform linux -- Python 3.11.0, pytest-7.2.2, pluggy-1.0.0
rootdir: /app
plugins: time-machine-2.9.0
collected 366 items
tests/test_last_day.py ......................................................................................................................................................................................................................................................................................... [ 76%]
..................................................................................... [100%]
================================================================================================================================================= 366 passed in 1.06s ==================================================================================================================================================
$ docker exec -it faking_time_est pytest /app/tests/test_last_day.py::test_get_text_for_day_time_machine
================================================================================================================================================= test session starts ==================================================================================================================================================
platform linux -- Python 3.11.0, pytest-7.2.2, pluggy-1.0.0
rootdir: /app
plugins: time-machine-2.9.0
collected 366 items
tests/test_last_day.py ......................................................................................................................................................................................................................................................................................... [ 76%]
..................................................................................... [100%]
================================================================================================================================================= 366 passed in 0.23s ================================================================================================================================================== Conclusion
In this article, we went through a simple implementation of a function which is checking if a certain datetime is the last day of the month, experimented with freeze time, libfaketime, and tested with different time zones. The GitHub repos of the utilities used are:
The above code is available at Faking-time-demo.
As a final tip, it is generally recommended to save timestamps in their UTC value or use dedicated types such as timestamp with time zone, according to Postgres.






