I would expect the local time to be 2100-12-31 23:59:59+01:00 and the conversion back to UTC to preserve the original instant. Instead, both implementations produce the wrong wall time and move the instant forward by an hour on the round trip.
This footer describes permanent DST at UTC+01, with a dummy standard offset of UTC+02, using the all-year DST convention described in RFC 9636, section 3.3.1. The end of DST for 2100 is December 31 at 23:00 DST, or 22:00 UTC. The start of DST for 2101 is January 1 at 00:00 standard time, which is the same instant. There is no intervening period of standard time.
The pure-Python UTC lookup and the C UTC lookup only evaluate the rules for the UTC year. At 22:59:59 UTC, that year's DST period has ended, but the next year's period has already started. The lookup nevertheless selects the dummy UTC+02 offset and adds two hours. When the result's offset is subsequently queried using its local year, it gets UTC+01, which accounts for the inconsistent result above.
The original example was the XXX-2<+01>-1,0/0,J365/23 footer used for Africa/Casablanca in rearguard data. This reproducer uses J1 instead of 0 so it does not depend on the separate off-by-one bug in zero-based day rules, and constructs the file directly so no particular tzdata installation is required. I think the lookup needs to account for adjacent years' rules near a year boundary.
Activity
pythongh-159715: Explain year-boundary cases and lookup margins
pythongh-159715: Explain lookup margins where they are used
pythongh-159715: Cite the specification for transition hour limits