ciso8601: an ISO 8601 parser written in C for Python
Fast ISO8601 date time parser for Python written in C
At a glance
- What is it?
- ciso8601 converts ISO 8601 and RFC 3339 timestamp strings into Python datetime objects from a C extension, parsing a naive timestamp in tens of nanoseconds and far faster than the pure Python alternatives. The project is also unusually candid about when you should not use it.
- Who is it for?
- Adopt ciso8601 when you are parsing large volumes of ISO 8601 timestamps in a hot loop and the standard library cannot keep up, since the benchmark difference is measured in microseconds against nanoseconds. Do not adopt it for a service that parses a handful of strings, because the project itself points you at the standard library and a separate document on when not to use it, particularly since Python 3.11.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 134 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What it does, and the two functions you will actually call
ciso8601 converts ISO 8601 or RFC 3339 date time strings into Python datetime objects, and it is written as a C module, which the README says makes it much faster than other Python libraries.
The quick start is short:
% pip install ciso8601and the usage example shows both a formatted timestamp and the compact variant:
In [1]: import ciso8601
In [2]: ciso8601.parse_datetime('2014-12-05T12:30:45.123456-05:30')
Out[2]: datetime.datetime(2014, 12, 5, 12, 30, 45, 123456, tzinfo=pytz.FixedOffset(330))
In [3]: ciso8601.parse_datetime('20141205T123045')
Out[3]: datetime.datetime(2014, 12, 5, 12, 30, 45)Two things are worth noting in that output. The first call carries a UTC offset, and the result is an aware datetime with a fixed offset in the output, matching the offset in the input. The second call has no offset, and the result is a naive datetime. That distinction is not an implementation detail, it is the difference between a timestamp you can compare with other timestamps and one that silently assumes it is in local time.
The supported Python versions are listed as cPython 3.8 through 3.14, which is a wide range for a C extension and tells you the project keeps up with new interpreter releases.
The package is MIT licensed, the last push was on 2026-06-11, and the recent releases are v2.3.3 on 2025-08-20, v2.3.2 on 2024-12-09 and v2.3.1 on 2023-10-30.
The error handling contract, which is the real design decision
Starting in v2.0.0, ciso8601 offers strong guarantees when it comes to parsing strings, and the guarantee is stated precisely.
parse_datetime takes a string and either returns a properly parsed Python datetime, if and only if the entire string conforms to the supported subset of ISO 8601, or raises a ValueError with a description of the reason why the string does not conform to the supported subset of ISO 8601.
The emphasis on the entire string is the point. A parser that accepts a valid prefix and ignores the rest is convenient until the day it silently mis-parses a malformed timestamp in production data. This one either returns a fully valid datetime or fails loudly.
The time zone rule is stated right after: if time zone information is provided, an aware datetime object is returned, otherwise a naive datetime is returned. There is no default, no implicit local time and no silently applied zone.
The supported subset wording also deserves attention. The guarantee is conformance to the subset of ISO 8601 that ciso8601 supports, not to ISO 8601 in full. A timestamp using a construct the library does not implement will raise a ValueError rather than being approximated, which is the behaviour you want, but it does mean the subset boundary determines what your input may contain.
The practical consequence is that a caller needs to decide how to treat a ValueError. Most code will let it propagate, which is correct for a parsing library, and a service that ingests untrusted timestamps will need to catch it at the boundary.
The benchmark, and the awkward truth in the middle of it
The README includes a benchmark table for parsing a timestamp with no time zone information, for example 2014-01-09T21:48:00, across Python 3.8 to 3.14, with a relative slowdown column measured against ciso8601 on the latest Python.
The headline numbers are clear. ciso8601 is in the tens of nanoseconds on every version, ranging from 59.5 nsec on Python 3.11 to 93.5 nsec on Python 3.8. The pure Python alternatives are in microseconds, with python-dateutil at 3.14 to 4.22 usec, iso8601 at 4.97 to 5.7 usec and isodate at 5.24 to 6.07 usec.
The row that deserves a second look is backports.datetime_fromisoformat, at 0.9x relative slowdown on the newest Python. That is the standard library's fromisoformat backported, and on recent versions it is roughly on par with ciso8601 rather than two orders of magnitude slower.
That is why the README carries a section on when you should not use ciso8601. The project says it is not necessarily the best solution for every use case, especially since Python 3.11, and links to a separate document in the repository, why_ciso8601.md, titled Should I use ciso8601?. The standard library's datetime.fromisoformat has been improving steadily, and the benchmark shows a library that was once decisively faster is now at parity on the newest interpreter.
So the honest reading of the table is that ciso8601's advantage is large and consistent against the pure Python parsers, and narrow or absent against the standard library on recent Python. The 3.8 and 3.9 columns show the same story: on those versions, the backport is 71.6 and 79 nsec against ciso8601 at 85.7 and 93.5 nsec, so the backport wins there too, and ciso8601's advantage on old interpreters is mostly against libraries that were always slow.
The relative slowdown figures for the pure Python options range from 7.8x for udatetime to 175.7x for aniso8601.
The C implementation, and the environment switch behind it
The repository layout shows a small, hand-written C extension rather than a binding to a date library. There is module.c, timezone.c and isocalendar.c as the three source files of the extension, with timezone.h and isocalendar.h alongside them, plus a ciso8601/ directory for the Python side, a tests/ directory, a benchmarking/ directory and a generate_test_timestamps.py script for producing test inputs.
The setup.py confirms the shape. The extension is named ciso8601 and is built from those three sources, with the version passed in as a CISO8601_VERSION macro.
The second macro is more interesting. CISO8601_CACHING_ENABLED is read from an environment variable of the same name, defaulting to enabled, and it is compiled in as a define. So the caching behaviour of the parser is a build-time choice, not a runtime one. A deployment that cannot tolerate a growing cache, or that wants to measure without it, can rebuild the extension with the macro off.
The compiler warning setup is also unusual, and the comments explain why. The project wants all warnings treated as errors, to catch potential issues during development and at pull request review time. But since ciso8601 is a source distribution, exotic compiler configurations can cause spurious warnings that would fail the installation, so warnings are only treated as errors during development. The gate is the STRICT_WARNINGS environment variable, and when it is set to 1 the setup adds the appropriate flag for whichever compiler is in use, since the cl.exe compiler on Windows and the gcc compilers do not share flags and there is no easy way to tell which compiler is being used.
That is a careful piece of packaging logic for a small library, and it is the kind of thing that shows up only when the project is actually distributed as source across many platforms.
Wheels for every platform, and the PyPy and Windows exclusions
The pyproject.toml configures cibuildwheel, and the matrix tells you the full shape of the distribution.
The build list covers cp38 through cp314 for CPython and pp38 through pp311 for PyPy, with pypy and pypy-eol enabled. On Linux the architectures are x86_64 and aarch64. On macOS they are x86_64, universal2 and arm64, with the MACOSX_DEPLOYMENT_TARGET set to 11.0 and the PyPy builds skipped. On Windows the skips are pp*-win*, *-win32, cp39-win_arm64 and cp310-win_arm64.
The Windows exclusions are the interesting ones, because they are not arbitrary. 32-bit Windows is skipped entirely, and the 64-bit ARM builds are skipped for CPython 3.9 and 3.10 specifically, which means that Windows on ARM is supported from 3.11 upward but not before. If you are building for Windows on ARM, the version of Python you run decides whether a wheel exists for you.
The macOS universal2 target is the other one worth noting, since a universal2 wheel covers both Intel and Apple Silicon in a single artifact, with the deployment target pinned so the binary runs on the stated minimum macOS version rather than whatever the build machine happened to have.
The rest of the configuration in pyproject.toml is linting rather than packaging: pylint with a max line length of 120 and a set of disabled messages, and autopep8 with the same line length, in-place and recursive mode, and aggressive level 3.
Upgrading, and when this library is the wrong answer
Two sections of the README exist specifically to talk you out of using the library, and both are worth reading before you add the dependency.
The first is the migration note. Version 2.0.0 of ciso8601 changed the core implementation, this was not entirely backwards compatible, and care should be taken when migrating. The migration guide lives in the CHANGELOG, which the README links. A library that rewrites its core in a major version and tells you so plainly is being straight with its users, and the breaking change is worth reading rather than discovering in a test suite.
The second is the section on when you should not use ciso8601, which points to the why_ciso8601.md document in the repository. The README's own framing is that it is not necessarily the best solution for every use case, especially since Python 3.11.
The reasons are visible in the benchmark if you read it carefully. The standard library has been catching up, and the backported fromisoformat is at parity with ciso8601 on the newest interpreters. If your workload parses a few thousand timestamps at startup, the difference between 70 nanoseconds and 70 nanoseconds is irrelevant, and you have taken on a C extension dependency with a compilation step, a platform matrix and a major version migration for nothing.
ciso8601 earns its place when the parsing is genuinely hot. A log processing pipeline, a high volume event consumer or a batch job over millions of rows is where the microsecond to nanosecond difference compounds into wall clock time, and where the C extension's cost of installation is amortised immediately.
The project is by Close, and the README includes the company's hiring note in its usual place, offering great engineers a role. The last push was on 2026-06-11 and the newest release is v2.3.3 from 2025-08-20, so the project is maintained on a steady rather than a fast cadence.
Editorial conclusion
Adopt ciso8601 when you are parsing large volumes of ISO 8601 timestamps in a hot loop and the standard library cannot keep up, since the benchmark difference is measured in microseconds against nanoseconds. Do not adopt it for a service that parses a handful of strings, because the project itself points you at the standard library and a separate document on when not to use it, particularly since Python 3.11. Before you upgrade from a 1.x version, read the migration guide in the CHANGELOG, because version 2.0.0 changed the core implementation and was not entirely backwards compatible.
Frequently asked questions
What does ciso8601 do?
It converts ISO 8601 and RFC 3339 date time strings into Python datetime objects. Since it is written as a C module, the README says it is much faster than other Python libraries, and the benchmark shows a naive timestamp parsed in tens of nanoseconds.
How does ciso8601 handle invalid timestamps?
Since v2.0.0 the guarantee is strong. parse_datetime returns a properly parsed datetime if and only if the entire string conforms to the supported subset of ISO 8601, and otherwise raises a ValueError describing why it does not conform.
When should I not use ciso8601?
The README says it is not necessarily the best solution for every use case, especially since Python 3.11, and points to a why_ciso8601.md document in the repository. The benchmark shows the standard library backport at parity with ciso8601 on the newest Python versions.
What changed in ciso8601 version 2.0.0?
It changed the core implementation, and the README says this was not entirely backwards compatible and that care should be taken when migrating. The migration guide is in the CHANGELOG, and v2.0.0 is also where the strong parsing guarantees were introduced.
Which Python versions and platforms does ciso8601 support?
It is tested with cPython 3.8 through 3.14. The cibuildwheel configuration builds for Linux on x86_64 and aarch64, for macOS on x86_64, universal2 and arm64, and for Windows, where 32-bit builds and the CPython 3.9 and 3.10 win_arm64 targets are skipped.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/closeio-ciso8601)Was this useful?