Skip to main content

pytest-tzcheck

Run your tests in the timezone CI uses, and find the ones that only pass in yours.

"Passes on my machine, fails in CI" often comes down to the local timezone: your laptop is in Asia/Jerusalem, the CI runner is in UTC. pytest --tzcheck runs every test in both zones and tells you which ones change outcome, before you push.

$ pytest --tzcheck tests/server/schemas/test_schedules.py
tzcheck: local (Asia/Jerusalem), UTC
...
=========================== timezone-dependent tests ===========================
tests/server/schemas/test_schedules.py::TestRRuleSchedule::test_rrule_schedule_preserves_and_localizes_rdates   passed: local (Asia/Jerusalem)   failed: UTC

Install

pip install pytest-tzcheck

Python 3.9+, pytest 7+, Linux or macOS (it needs time.tzset(), which Windows does not have).

Usage

pytest --tzcheck                                    # local zone + UTC (the "ci" preset)
pytest --tzcheck --tzcheck-zones=wide               # local, UTC, +14h, half-hour offsets, southern DST
pytest --tzcheck --tzcheck-zones=UTC,America/New_York
pytest --tzcheck -k "tz=UTC"                        # only the UTC runs

Or turn it on in config:

[pytest]
tzcheck = true
tzcheck_zones = ci
Zone list Zones
ci (default) local, UTC
wide local, UTC, Pacific/Kiritimati (UTC+14, the local date is not UTC's most of the day), America/St_Johns (half-hour offset), Australia/Lord_Howe (half-hour DST shift, southern hemisphere)

local is the environment as you started pytest. Presets and IANA names can be mixed. Zones with the same UTC offsets all year are merged, so on a UTC CI runner --tzcheck does not double the run.

Opt a test out with @pytest.mark.no_tzcheck. A test can ask which zone it runs in through the tzcheck_zone fixture.

A failure in any zone fails the run, so --tzcheck works as a CI gate as well as a local check.

How it works

  1. Each test is parametrized over the zones (test_x[tz=local], test_x[tz=UTC]), so the runs are ordinary pytest items: -k, --lf, JUnit XML and pytest-xdist all work.
  2. Around each run (setup, call and teardown) the plugin sets TZ, calls time.tzset(), and restores both afterwards. Subprocesses started by the test inherit the zone. If tzlocal is imported, its cache is reloaded.
  3. The outcome of each run is recorded, and the summary lists every test whose outcome differs between zones. A test that fails everywhere is not listed: that is a bug, but not a timezone bug.

Case study: a prefect PR

This plugin started from a real bug. While fixing RDATE/EXDATE handling in prefect's RRuleSchedule (PrefectHQ/prefect#23436), the new regression tests passed locally (Asia/Jerusalem) and failed in CI (UTC).

The cause: dateutil parses a timestamp ending in Z as tzutc(), unless the machine's local zone is UTC, in which case it returns tzlocal(). The fix checked isinstance(dt.tzinfo, tzutc), which is true in Jerusalem and false in UTC. Nothing in that code path calls a local-time function; dateutil just compares against time.tzname.

Prefect's schedule tests on the commit that failed CI (bb4f72475):

$ pytest tests/client/schemas/test_schedules.py tests/server/schemas/test_schedules.py
155 passed, 6 skipped, 2 xfailed

$ pytest --tzcheck tests/client/schemas/test_schedules.py tests/server/schemas/test_schedules.py
=========================== timezone-dependent tests ===========================
tests/client/schemas/test_schedules.py::TestRRuleScheduleFromRRule::test_rruleset_keeps_rdate_and_exdate_instants_in_another_timezone   passed: local (Asia/Jerusalem)   failed: UTC
tests/server/schemas/test_schedules.py::TestRRuleSchedule::test_rrule_schedule_preserves_and_localizes_exdates   passed: local (Asia/Jerusalem)   failed: UTC
tests/server/schemas/test_schedules.py::TestRRuleSchedule::test_rrule_schedule_preserves_and_localizes_rdates   passed: local (Asia/Jerusalem)   failed: UTC
tests/server/schemas/test_schedules.py::TestRRuleSchedule::test_serialization_preserves_rdate_and_exdate_in_another_timezone   passed: local (Asia/Jerusalem)   failed: UTC
tests/server/schemas/test_schedules.py::TestRRuleSchedule::test_serialization_preserves_rrules_rdates_exrules_exdates   passed: local (Asia/Jerusalem)   failed: UTC
5 of 163 tests changed outcome with the timezone
5 failed, 305 passed, 12 skipped, 4 xfailed

On the fixed commit (9e24d058d, which checks the UTC offset instead of the tzinfo type):

$ pytest --tzcheck tests/client/schemas/test_schedules.py tests/server/schemas/test_schedules.py
tzcheck: no timezone-dependent tests (163 tests x 2 zones)
310 passed, 12 skipped, 4 xfailed

Why run every test, rather than detect the risky ones?

The cheaper design is to trace calls to local-time APIs (time.localtime, naive datetime.now(), astimezone() without arguments) and rerun only the tests that make them. It would have missed the bug above: in Jerusalem, dateutil only reads time.tzname, an attribute, so no call is made. Running each test in each zone and comparing outcomes has no false positives and no blind spots, at the cost of one extra run per zone. Linters such as ruff's DTZ rules cover the static side.

Limitations

  • Fixtures with module, class or session scope are created once, in the zone of the first test that uses them. Values computed at import time (a module-level tz.tzlocal(), a cached get_localzone() result) also stay in the first zone.
  • The zone is per process: threads running during the test see it too.
  • POSIX only.
  • libc silently treats an unknown TZ as UTC, so the plugin refuses zone names that are not in the system tz database.

Roadmap

  • --tzcheck-mode=probe: trace local-time calls with sys.monitoring (Python 3.12+) and rerun only those tests in the extra zones, as a fast pre-filter for large suites, with the blind spot above documented.
  • Run each zone at a few fixed instants (around midnight UTC, DST transitions), since many timezone bugs only show up at certain hours.
  • A JSON report for CI annotations.

Development

uv sync
uv run pytest
uv run ruff check . && uv run ruff format --check .

To try it on another project without installing it: PYTHONPATH=path/to/pytest-tzcheck/src pytest -p pytest_tzcheck.plugin --tzcheck.

Releasing

Pushing a v* tag runs .github/workflows/release.yml: it checks the tag matches the version in pyproject.toml, runs lint and tests, builds, and publishes to PyPI through trusted publishing (no API token).

uv version --bump patch        # or minor / major
git commit -am "Release $(uv version --short)"
git tag "v$(uv version --short)"
git push origin main --tags

License

MIT

Metadata

Release files for pytest-tzcheck 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pytest-tzcheck 0.1.0
File Size Uploaded Provenance
pytest_tzcheck-0.1.0.tar.gz 23.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for pytest-tzcheck 0.1.0
File Interpreter ABI Platform
pytest_tzcheck-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 32.1 kB

Release files / pytest_tzcheck-0.1.0.tar.gz

Download URL pytest_tzcheck-0.1.0.tar.gz
Size 23.0 kB
Tags Source
SHA-256 checksum
How to use checksums
03fcfbd739fbf36187609f8f996d8bf0872dec82062a36da3a6356980d33b8c6
BLAKE2b-256 checksum
How to use checksums
21d215c3d14977c4b3d82830a7cbf80808c31663c04231fa5403ce213d90a4da
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 Oct 19, 2026.

Transparency log

Release files / pytest_tzcheck-0.1.0-py3-none-any.whl

Download URL pytest_tzcheck-0.1.0-py3-none-any.whl
Size 9.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f4103db5758a835d7cd089e8d0ee91a03c6f8de1b6d39274347d0fc9e8de53c3
BLAKE2b-256 checksum
How to use checksums
d191f4a57372e72832434b2695d5e7c20b7b5c1f9325c4154eb861595590037d
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 Oct 19, 2026.

Transparency log

Security for pytest-tzcheck 0.1.0

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for pytest_tzcheck-0.1.0.tar.gz

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 Oct 19, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page