← Research index
INDEPENDENT NUMERICAL AUDIT13 September 2026

FinancePy Act/365L can fail when an optional date is omitted

DayCount.year_frac() documents an ordinary two-date mode where dt3 is absent. In FinancePy 1.0.1, an annual Act/365L interval that reaches February 29 can dereference that missing object instead of returning a fraction.

Xamit Kadirbekov
Xamit KadirbekovIndependent verification · GERO Research
Financial calculationsFinancePy 1.0.1Day-count conventions
STATUS · REPORTED UPSTREAM — CORRECTION SUBMITTEDReproduced from the published PyPI wheel. FinancePy PR #259 is open and mergeable; both official workflows pass. The correction has not been accepted or merged.

A one-minute explanation

Synthetic English narration using macOS Samantha. Original motion cards, no music or third-party imagery. Download the script, claim ledger, renderer and QA record.

Released-wheel reproduction

The documented ordinary mode sets dt3 to None. This synthetic call uses 1 December 2023 as the start, 1 March 2024 as the end, and annual Act/365L:

DayCount(DayCountTypes.ACT_365L).year_frac(
    Date(1, 12, 2023),
    Date(1, 3, 2024),
    freq_type=FrequencyTypes.ANNUAL,
)

FinancePy 1.0.1 raises AttributeError: 'NoneType' object has no attribute 'excel_dt'. Supplying dt3=dt2 returns (0.24863387978142076, 91.0, 366).

The independent result

Python’s standard-library calendar gives 91 actual days. The cited annual Act/365L definition uses 366 when the relevant period contains February 29. The expected fraction is therefore:

actual days       91
denominator      366
year fraction    91 / 366 = 0.24863387978142076

OpenGamma Strata also describes annual Act/365L in terms of the period end or next coupon date. For an ordinary complete period, the calculation end is that period end. For an accrued fraction, the separately supplied coupon-period end remains distinct.

Why the exception occurs

The implementation calculates a year from dt2 when dt3 is absent, but it does not assign a date to dt3. If a leap day is selected, the annual branch later evaluates:

if feb29 > dt1 and feb29 <= dt3:
    den = 366

The second comparison reaches Date.__le__ with dt3=None. Intervals that do not reach the relevant leap day can conceal the problem through boolean short-circuiting.

Submitted correction and falsification

FinancePy pull request #259 assigns dt2 to dt3 only when the optional reference-period end was not supplied. A caller-provided coupon end is left unchanged.

  • The focused day-count file reports 11 passes.
  • The complete local unit suite reports 959 passes, with four pre-existing LSMC RankWarning messages.
  • Restoring the former None handling reproduces the exception.
  • A separate control confirms that an explicitly later coupon end still selects the accrued-fraction denominator.
  • Both official upstream workflows report success on commit a93243c4982dc77432d3405d478c34998e0f04a6.

At publication time the pull request remains open and unmerged. Passing checks establish the status of the submitted change; they do not imply maintainer acceptance.

Evidence and limits

The public PyPI wheel has SHA-256 110e784122f485ae207239f44af473334f418d81bcbb958f5eda4e76956feca5. The audited day_count.py inside it has SHA-256 da564a6105eecde08b0f302b715a76fd5798e46b565409c2b5fb7de897ed70a7. The same control-flow failure remains at upstream base commit 2b9227fea9d832c4033421d6cd53a54316414fca.

A bounded search of FinancePy issues and pull requests for four direct formulations found no prior report. That is not a worldwide priority guarantee.

All dates are synthetic. The report establishes a released library failure. It does not establish institutional use, downstream exposure, customer loss, security impact, or production deployment.

The public reproducer and report are supplied under GPL-3.0-or-later. Source excerpts retain their upstream licensing.