Repository navigation
Conversation
pganssle
commented
Oct 25, 2026
Use structural protocols and overloads to preserve the library's permissive inputs, with type-only imports guarded to limit import overhead. Package a py.typed marker and check the annotations with pyrefly in CI. Add pytest cases that pair inferred-type expectations with runtime checks for duck-typed inputs. Existing unittest tests remain unannotated.
Use narrower internal types and control flow to remove unnecessary casts. The remaining casts use a shared no-op fallback from _common, with the real typing.cast imported only for type checking. Guard overload declarations with TYPE_CHECKING and separate missing transition information from ordinary offset records so these annotations do not require runtime imports from typing.
Describe the fromutc decorator's input and output relationship, include arithmetic fallback overloads, and preserve structural inputs when narrowing internal state. Use the existing stream capability checks as a type guard and carry narrowed recurrence values through their consumers. Keep the remaining casts at dynamic dispatch and external data boundaries, including timezone lookups with a None key. Add contract checks for these lookups, datetime subclasses, arithmetic fallbacks and offset precedence.
pganssle
force-pushed
the
add-inline-type-hints
branch
from
October 25, 2026 20:33
28c4ab8 to
8bf5f7a
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary of changes
Add inline type annotations and a
py.typedmarker so downstream users can use the library's annotations. The signatures use structural protocols and overloads to preserve duck typing, withtox -e typingchecking the implementation and type expectations in CI.Annotation-only imports and overload declarations are guarded by
TYPE_CHECKING. Casts that remain use a shared no-op fallback at runtime.Related to #383. Earlier approaches are #754's generated stubs and #768's annotations for
utils.py.Comparison with typeshed
These annotations were compared with the typeshed stubs distributed in
types-python-dateutil==2.9.0.20261021. The implementation is the source oftruth where they differ:
utils.default_tzinfoaccepts objects withtzinfoand an appropriatereplacemethod, includingdatetime.time, preserves their type, andaccepts
Noneas the replacement zone.utils.within_deltaaccepts compatiblesubtraction and comparison operations, including numbers and dates, rather
than requiring datetimes and timedeltas.
parser.parseaccepts bytearrays and streams whosereadmethod requiresa size argument. Parser configuration and time zone lookups use structural
interfaces. Time zone mappings may return strings or
None, as well asinteger offsets and
tzinfoinstances.permits a bytes separator for
isoparser, but the implementation requiresa string when a separator is specified.
parse_tzstrreturns atzutcortzoffsetinstance.relativedeltaaccept floats, and weekday arguments mayprovide
weekdayandnattributes without inheriting from dateutil'sweekday class. Scaling a
relativedeltaaccepts strings, bytes,bytearrays and objects providing
__float__or__index__, because itconverts the operand with
float.relativedeltadoes not promise to preserve an arbitrarydate subclass: adding a time component can promote a date to a datetime.
datetime specified in typeshed.
countexhausts the recurrence and returnsan integer.
replaceconstructs anrrule, rather than preserving anarbitrary subclass.
Recurrence sets accept any iterable of datetimes as a rule.
Frequencies accept integer variables, while invalid values remain runtime
errors. Nonzero integer
untilvalues are not supported.tzfileaccepts read-only binary streams without seeking, exposes itskey, and accepts the constructor'skeyargument. Its ambiguity methoddoes not accept the old
idxargument. These differences also reflectchanges since the version covered by the stubs.
total_seconds.Range transitions accept objects supporting addition to a datetime, and
fixed ranges return
Nonefromtransitions. Fixed-offset names may beNone; fixed-zone offset methods always return a timedelta.tzstr.instancetakes a TZ string andposix_offset, and returns atzstr. The typeshed signature instead describestzoffset.instance.tz.enfoldaccepts objects supportingreplace(fold=...)and preservestheir type.
zoneinfo.rebuildignores its arguments, so its annotationsdo not impose the restrictions of its former implementation.
Import benchmarks against master and before cast removal
These measurements compare master (
27ecba297e3e33d2e46c0f34f06452d2284bf029), the rebased PR (8bf5f7ac), and the annotation revision before cast removal (7cf76f2e). All three source snapshots are on the same filesystem and use the same_version.pygenerated by the current Hatchling build. The comparison with the pre-removal revision therefore isolates source changes under the current version-file format; it does not reproduce its former packaging environment. This replaces the earlier table, which compared snapshots on different filesystems.Each measurement uses a fresh process on CPython 3.11.17 or 3.15.0, with 60 samples and two warm-up rounds. Revision and module order are randomized, the process is pinned to one CPU, and bytecode caches are warm. Processes use
-SandPYTHONHASHSEED=0. The timer surroundsimportlib.import_module, including dependencies but excluding interpreter startup. Times below are medians in milliseconds; positive changes mean slower imports.Python 3.11
dateutildateutil._commondateutil.easterdateutil.parserdateutil.parser.isoparserdateutil.relativedeltadateutil.rruledateutil.tzdateutil.utilsdateutil.zoneinfoPython 3.15
dateutildateutil._commondateutil.easterdateutil.parserdateutil.parser.isoparserdateutil.relativedeltadateutil.rruledateutil.tzdateutil.utilsdateutil.zoneinfoThe earlier comparison against
64914d9bused the old setuptools-generated version file. There, importing__future__accounted for most of the root-package overhead. Master's new Hatchling build generates a_version.pythat already imports__future__, so that cost is shared by the baseline and PR in this comparison. Preloading__future__before starting the timer gives the following root-package measurements:Larger modules also load more bytecode and construct annotation metadata and additional helper definitions. An isolated experiment stripping signature annotations from compiled code reduced some of that overhead, but doesn't establish an exact breakdown. Small differences vary between runs; the percentages for the root package should be read alongside their absolute cost.
Remaining casts
The library now contains 13 typing casts, down from 80 in
6ccb96cband 133 in the initial annotation commit. These use the shared runtime fallback indateutil._common. The separatectypes.castin the Windows module performs a pointer conversion and is not a typing cast.parser.__deprecate_private_classtz._factoriesinstancemethod.rruleweekday normalizationnattribute, including objects that are also iterable. The checker cannot narrow both branches of that structural dispatch._ymd.append__len__, followed by string-like operations, or numeric comparison otherwise. Replacing it with anisinstance(str)check would change duck typing. The string branch now uses a protocol rather than pretending the value is necessarily astr.parsertimezone lookupNonekey is valid even for a lookup otherwise typed with string keys. The checker cannot express that relationship between__contains__andget.getattr(input, "read", identity)()selects the reader's result or the original text/bytes. This also preserves reader methods on string subclasses.total_seconds, and the existing code falls back to the original number when the method raisesAttributeErrororTypeError. The cast describes that fallback, shared by all three callers.Other casts were removed through narrower signatures, constrained type variables, a stream capability guard, explicit state invariants, and control flow that carries narrowed values to their uses. No new type-checker suppressions were added.
Remaining typing diagnostics
The
tzicalparser initializes component state and variables together, then uses that state to guard later access. Pyrefly does not establish that relationship across loop iterations, so seven uses have localunbound-namesuppressions. A reduced example also produces a possibly-unbound diagnostic in pyright, while mypy accepts it.One other suppression preserves an existing component-indexing error in the fallback for dates before the onset of an all-DST zone. This PR does not change that behavior.
Pull Request Checklist