💤 SleepFake: Time Travel for Your Tests
Ever wish your tests could skip the waiting but keep correct time behavior? SleepFake patches time.sleep and asyncio.sleep so tests return instantly while frozen time moves forward exactly as requested. Async timeouts (asyncio.wait_for, asyncio.timeout, loop.call_later) fire on the fake clock too.
📦 Install
pip install sleepfake
Python 3.10 to 3.15, including free-threaded builds (3.13t+). The pytest plugin registers itself.
⚡ Quick start: global autouse
Make SleepFake apply to every test. Add to pyproject.toml:
[tool.pytest.ini_options]
sleepfake_autouse = true
Now regular tests skip sleeps:
import time
def test_retry():
start = time.time()
time.sleep(30) # returns instantly
assert time.time() - start >= 30
Async works the same way, timeouts included:
import asyncio
import pytest
async def test_polling():
start = asyncio.get_running_loop().time()
await asyncio.sleep(10) # returns instantly
assert asyncio.get_running_loop().time() - start >= 10
async def test_gives_up():
with pytest.raises(TimeoutError):
await asyncio.wait_for(asyncio.Event().wait(), timeout=60) # ~20 ms, not 60 s
pytest-asynciousers: addasyncio_mode = "auto"(or mark tests with@pytest.mark.asyncio) so pytest collects async tests.
🧭 Choose your usage style
| Use case | Best option |
|---|---|
| Apply everywhere (most teams) | Global autouse (sleepfake_autouse = true or --sleepfake) |
| Per-test explicit control | sleepfake fixture |
| Decoration-style usage | @pytest.mark.sleepfake |
| Non-pytest scripts / direct control | SleepFake context manager |
All pytest styles share one instance per test: combining them never patches twice.
📚 Usage
Context manager
import asyncio
import time
from sleepfake import SleepFake
# Sync
with SleepFake():
start = time.time()
time.sleep(10) # returns instantly
assert time.time() - start >= 10
# Async: `async with` or plain `with` both work
async def main():
async with SleepFake():
start = asyncio.get_running_loop().time()
await asyncio.sleep(5) # returns instantly
assert asyncio.get_running_loop().time() - start >= 5
Keep some modules on real clocks with ignore:
with SleepFake(ignore=["my_project.telemetry"]):
...
Fixture (sleepfake)
import asyncio
import time
def test_retry_logic(sleepfake):
start = time.time()
time.sleep(30) # instantly skipped
assert time.time() - start >= 30
async def test_polling(sleepfake):
start = asyncio.get_running_loop().time()
await asyncio.gather(asyncio.sleep(1), asyncio.sleep(5), asyncio.sleep(3))
# All three complete instantly; the clock sits at +5 s
assert asyncio.get_running_loop().time() - start >= 5
The fixture yields the active SleepFake; sleepfake.mock_sleep(60) advances the clock by hand.
Deprecated:
asleepfakestill works but warns. Usesleepfakefor sync and async tests.
Marker (@pytest.mark.sleepfake)
import time
import pytest
@pytest.mark.sleepfake
def test_marked():
start = time.time()
time.sleep(100)
assert time.time() - start >= 100
Works on async tests, classes and modules (pytestmark) too.
Global autouse, opt-out and ignores
Enable it in config or on the command line:
# pyproject.toml
[tool.pytest.ini_options]
sleepfake_autouse = true
sleepfake_ignore = ["my_project.telemetry", "my_project.metrics"]
pytest --sleepfake --sleepfake-ignore my_project.telemetry
Opt a single test out with @pytest.mark.no_sleepfake (it has no effect if the test explicitly requests the sleepfake fixture).
Directory-scoped ignores go in a conftest.py; the nearest one wins:
# conftest.py
pytest_sleepfake_ignore = ["my_project.telemetry"]
Function-scoped fixtures are set up and torn down with the fake active; session-, module- and class-scoped fixtures stay on real time.
Async timeouts and the autojump threshold
asyncio.sleep always wakes instantly. Loop timers (asyncio.wait_for, asyncio.timeout, loop.call_later) fire once the event loop has been idle for autojump_threshold real seconds (default 0.02). That short grace period lets real I/O, such as a local test server, answer before a timeout is forced.
async def test_timeout(sleepfake):
with pytest.raises(TimeoutError):
async with asyncio.timeout(2):
await asyncio.sleep(10) # the clock stops at +2 s, like real asyncio
Tune it per instance or per project:
SleepFake(autojump_threshold=0) # jump as soon as the loop is idle
SleepFake(autojump_threshold=math.inf) # never jump: timers only fire when sleeps move the clock
[tool.pytest.ini_options]
sleepfake_autojump_threshold = "0.5"
🛠️ Options reference
| Where | Option | Purpose |
|---|---|---|
SleepFake(...) |
ignore: list[str] |
Module prefixes that stay on real clocks. |
SleepFake(...) |
autojump_threshold: float |
Real idle seconds before loop timers fire (default 0.02, 0, math.inf). |
| pytest config | sleepfake_autouse = true |
Apply SleepFake to every test. |
| pytest config | sleepfake_ignore |
Module prefixes that stay on real clocks, for every test. |
| pytest config | sleepfake_autojump_threshold |
Same as the constructor argument, for every test. |
| pytest CLI | --sleepfake |
Same as sleepfake_autouse = true. |
| pytest CLI | --sleepfake-ignore MODULE |
Add an ignored prefix (repeatable). |
conftest.py |
pytest_sleepfake_ignore |
Ignored prefixes for that directory subtree (a string or an iterable). |
| marker | @pytest.mark.sleepfake |
Apply SleepFake to one test, class or module. |
| marker | @pytest.mark.no_sleepfake |
Opt one test out of global autouse. |
Every ignore list is merged with DEFAULT_IGNORE = ["_pytest.timing", "pytest_timeout"], which keeps pytest's --durations and pytest-timeout on real clocks. Disable the plugin with -p no:sleepfake.
🧪 How it works
| Aspect | Detail |
|---|---|
| Clock | freezegun freezes time.time, time.monotonic, datetime.now... and so the event loop's clock |
| Sync sleep | time.sleep(n) ticks the frozen clock by n (thread-safe) |
| Async sleep | Once the event loop is idle, the clock jumps to the earliest pending deadline and that sleep wakes, so concurrent sleeps resolve in order |
| Loop timers | BaseEventLoop.call_at is patched; after autojump_threshold real idle seconds the clock jumps to the next timer |
| Module aliases | from time import sleep / from asyncio import sleep bindings in sys.modules are swapped on entry and restored on exit |
| Nesting | Contexts nest; the innermost one drives the clock |
⚠️ Limitations
- Local bindings. A
sleepcaptured in a local variable before the context starts (_sleep = time.sleep) keeps calling the real function. - Real I/O under a timeout. If real I/O takes longer than
autojump_thresholdinside await_for/timeout, the timeout fires on the fake clock. Raise the threshold, or set it tomath.inf. - Other event loops. Timer autojump reads asyncio's pure-Python loop internals. On other loops (e.g. uvloop)
asyncio.sleepis still faked, but loop timers need real time. - Timer precision. A timer jump lands 1 µs after the deadline: asyncio only runs a timer once the clock is strictly past it.
- Shared clock. The frozen clock is global: every thread and every event loop sees the same time.
🆚 Alternatives
| Tool | Fakes time.sleep |
Fakes asyncio.sleep / loop timers |
Freezes datetime / time.time |
|---|---|---|---|
| SleepFake | ✅ | ✅ | ✅ (via freezegun) |
| freezegun | ❌ (really sleeps) | ❌ | ✅ |
| time-machine | ❌ | ❌ | ✅ |
| looptime | ❌ | ✅ (asyncio loop time) | ❌ |
🤝 Contributing
make dev-install # uv sync + prek git hooks
make test-all # ruff + ty, then tests
make cov # tests under coverage (100% required)
make test-all-python # 3.10 to 3.15 and 3.14t
PRs and issues welcome. See CHANGELOG.md for release notes.
Metadata
Release files for sleepfake 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sleepfake-2.0.0.tar.gz | 14.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sleepfake-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 28.9 kB
Release files / sleepfake-2.0.0.tar.gz
| Download URL | sleepfake-2.0.0.tar.gz |
|---|---|
| Size | 14.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b6af5ee4408f1d203ec592fd5225ba8a39751f69d05988797002432226e675db
|
|
BLAKE2b-256 checksum How to use checksums |
d6dcb6fcd9db0fb2ecd710615cbf2f49bb109f7d318f3be9ec8faff44891b2c1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / sleepfake-2.0.0-py3-none-any.whl
| Download URL | sleepfake-2.0.0-py3-none-any.whl |
|---|---|
| Size | 14.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0296dd9b643edd0ebf63c28eb0f5006f9213ef5e9ed26a558769c51c3fe7b237
|
|
BLAKE2b-256 checksum How to use checksums |
922a7c2d879a4b54b4be1bca61ad868d2b525ac786f289897c45b00e4cd579e0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency log