Migrating from freezegun or libfaketime¶
freezegun has a useful API, and python-libfaketime copies some of it, with a different function name.
time-machine also copies some of freezegun’s API, in travel()'s destination, and tick arguments, and the shift() method.
There are a few differences:
time-machine’s
tickargument defaults toTrue, because code tends to make the (reasonable) assumption that time progresses whilst running, and should normally be tested as such. Testing with time frozen can make it easy to write exact assertions, but it’s quite artificial. Write assertions against time ranges, rather than against exact values.freezegun interprets dates and naive datetimes in the local time zone (including those parsed from strings with
dateutil). This means tests can pass when run in one time zone and fail in another. time-machine instead interprets dates and naive datetimes in UTC so they are fixed points in time. Provide time zones where required.freezegun’s
tick()method has been implemented asshift(), to avoid confusion with thetickargument. It also requires an explicit delta rather than defaulting to 1 second.freezegun’s
tz_offsetargument is not supported, since it only partially mocks the current time zone. Time zones are more complicated than a single offset from UTC, and freezegun only uses the offset intime.localtime(). Instead, time-machine will mock the current time zone if you give it adatetimewith aZoneInfotimezone.
Some features aren’t supported like the auto_tick_seconds argument.
These may be added in a future release.
If you are only fairly simple function calls, you should be able to migrate by replacing calls to freezegun.freeze_time() and libfaketime.fake_time() with time_machine.travel().
Migration CLI¶
time-machine comes with a command-line interface to help you migrate from freezegun.
It performs partial replacements on your code to update it to use time-machine’s API.
It may leave your code in a broken state, for example where an import of freezegun has been replaced but calls using it remain—it’s recommended you have a good linting setup to find these, and then you can manually fix them up.
To help with this, the tool reports freezegun-related usages that it recognizes but cannot migrate, with their positions:
$ python -m time_machine migrate example/tests.py
Rewriting example/tests.py
example/tests.py:9:2: freeze_time usage not migrated
These reports are heuristic and may occasionally flag unrelated code that reuses a freezegun-related name, such as a function parameter that shadows an imported freeze_time.
Files that cannot be parsed are also reported, and otherwise skipped. Since the tool parses files with the Python version it runs on, run it with a version at least as new as the target project uses.
The tool edits files in place, reporting those that it changes. It’s recommended you start from a clean, committed state in your version control system, so you can easily revert any broken changes.
Run with uv¶
If you have uv installed, you can use its uvx command to install and run the tool in one go:
$ uvx --from 'time-machine[cli]' python -m time_machine migrate example/tests.py
Replace example/tests.py with one or more target files.
Run directly¶
To install the tool before using it, first install time-machine with its cli extra.
For example, with Pip:
$ python -m pip install time-machine[cli]
Then, run the migrate subcommand of the module on target files:
$ python -m time_machine migrate example/tests.py
Rewriting example/tests.py
Replace example/tests.py with one or more target files.
Run against multiple files¶
To run the tool against all files from your Git repository, follow this blog post.
Changes¶
The tool makes the below changes, grouped here by the kind of code they apply to.
Import updates¶
import freezegun->import time_machine:-import freezegun +import time_machine
from freezegun import freeze_time->import time_machine:-from freezegun import freeze_time +import time_machine
Aliased imports like
import freezegun as fgorfrom freezegun import freeze_time as ft->import time_machine. The alias is dropped, since calls using it are migrated to use thetime_machinemodule, per the below:-import freezegun as fg +import time_machine -@fg.freeze_time("2023-01-01") +@time_machine.travel("2023-01-01", tick=False) def test_function(): ...
from freezegun import freeze_time, FakeDate->import time_machineplusfrom freezegun import FakeDate, keeping the other imported names:-from freezegun import freeze_time, FakeDate +import time_machine +from freezegun import FakeDate
freeze_time() calls¶
In function decorators, class decorators, and context managers:
freeze_time(...)->time_machine.travel(...). This change is applied only whenfreeze_time()is called with a single positional argument and only supported keyword arguments:tick,tz_offsetwith a literal zero value,real_asyncio, andignore. Iftickis passed, it is kept as-is, otherwise it is replaced withtick=False(matching freezegun’s default behaviour):-@freeze_time("2023-01-01") +@time_machine.travel("2023-01-01", tick=False) def test_function(): ... -@freeze_time("2023-01-01", tick=True) +@time_machine.travel("2023-01-01", tick=True) def test_function2(): ...
If no positional argument is passed,
Noneis added as the destination, meaning the current time, matching freezegun’s behaviour of freezing at the current time:-@freeze_time() +@time_machine.travel(None, tick=False) def test_function(): ...
tz_offset=0is dropped, since a zero offset has no effect.real_asynciois dropped, whatever its value, since time-machine does not mocktime.monotonic(), so asyncio event loops always see real time.ignoreis dropped, since it works around problems with freezegun’s module patching, which time-machine’s C-level mocking doesn’t have.-@freeze_time("2023-01-01", tz_offset=0, real_asyncio=True, ignore=["threading"]) +@time_machine.travel("2023-01-01", tick=False) def test_function(): ...
“Raw use” assignments that bind
freeze_time()to a variable for laterstart()andstop()calls: the assigned call is migrated as above, sincetravel()instances have the samestart()/stop()interface:def test_function(): - freezer = freeze_time("2023-01-01") + freezer = time_machine.travel("2023-01-01", tick=False) freezer.start() ... freezer.stop()
This applies to plain variables, like
freezer = freeze_time(...), checked within the enclosing function or module, and toself.attributes, checked across the enclosing class, so unittestsetUp()/tearDown()patterns are covered, including cleanup registrations likeself.addCleanup(self.freezer.stop):class TestSomething(TestCase): def setUp(self): - self.freezer = freeze_time("2023-01-01") + self.freezer = time_machine.travel("2023-01-01", tick=False) self.freezer.start() self.addCleanup(self.freezer.stop)
The migration only applies when the variable is used solely for
start()andstop(): as calls with no arguments in statements, or as bare references passed as call arguments. Other uses, such asmove_to()ortick()calls, prevent migration, since freezegun’s object has other methods with no equivalent ontravel().In context managers that bind the result with
as, additionally: calls of the bound variable’stick()method ->shift(), with freezegun’s default delta of one second made explicit. Calls of themove_to()method are left unchanged, since it behaves the same in both libraries:-with freeze_time("2023-01-01") as ft: +with time_machine.travel("2023-01-01", tick=False) as ft: ft.move_to("2023-06-01") - ft.tick() + ft.shift(1)
These changes are only applied when the bound variable is used solely for
move_to()calls andtick()calls as statements, sincetick()returns the new time whilstshift()returnsNone, and other freezegun attributes have no equivalent on the object thattravel()yields.
pytest.mark.freeze_time marker¶
The pytest.mark.freeze_time marker from pytest-freezegun or pytest-freezer is migrated: @pytest.mark.freeze_time(...) -> @pytest.mark.time_machine(...), the marker from time-machine’s pytest plugin.
This migration uses the same argument handling as for freeze_time() calls:
-@pytest.mark.freeze_time("2023-01-01")
+@pytest.mark.time_machine("2023-01-01", tick=False)
def test_function():
...
As well as in decorators, the marker is migrated in module-level and class-level pytestmark assignments, whether assigned alone or within a list or tuple of markers:
pytestmark = [
- pytest.mark.freeze_time("2023-01-01"),
+ pytest.mark.time_machine("2023-01-01", tick=False),
pytest.mark.django_db,
]
freezer fixture¶
The freezer fixture from pytest-freezegun or pytest-freezer is migrated to the time_machine fixture from time-machine’s pytest plugin.
In functions with an argument named freezer, the argument is renamed to time_machine and calls of the fixture’s methods are migrated:
freezer.move_to(...)->time_machine.move_to(..., tick=False), again matching freezegun’s default behaviour:-def test_function(freezer): - freezer.move_to("2023-01-01") +def test_function(time_machine): + time_machine.move_to("2023-01-01", tick=False)
tick=Falseisn’t added in functions using a migratedpytest.mark.freeze_timemarker, since there the fixture inherits thetickbehaviour from the marker:-@pytest.mark.freeze_time("2023-01-01") -def test_function(freezer): - freezer.move_to("2023-06-01") +@pytest.mark.time_machine("2023-01-01", tick=False) +def test_function(time_machine): + time_machine.move_to("2023-06-01")
freezer.tick()->time_machine.shift(1), as for context manager variables, again only for calls as statements:-def test_function(freezer): - freezer.tick() +def test_function(time_machine): + time_machine.shift(1)
Other uses of freezer are left unchanged, for your linter to flag.
freeze_time() calls within such functions are also left unchanged, because the renamed argument shadows the time_machine module.
Imports and uses of FrozenDateTimeFactory, freezegun’s class for the freezer fixture, often used to annotate the fixture argument, are also migrated: uses, including in string annotations, are rewritten to time-machine’s equivalent, TimeMachineFixture, and from time_machine import TimeMachineFixture replaces the freezegun import:
-from freezegun.api import FrozenDateTimeFactory
+from time_machine import TimeMachineFixture
-def test_function(freezer: FrozenDateTimeFactory):
- freezer.move_to("2023-01-01")
+def test_function(time_machine: TimeMachineFixture):
+ time_machine.move_to("2023-01-01", tick=False)
Note that the time_machine fixture doesn’t mock the time until its move_to() method is called, unlike freezer, which mocks from the start of the test.
Migrated tests that relied on that, for example by calling freezer.tick() before any move_to(), will need manual adjustment.
The tool is open to extension to cover other compatible changes—PRs welcome!