Maturity Analysis of Lease Liabilities: Building the Disclosure Table Under ASC 842 and IFRS 16
Build the lease maturity analysis: bucket undiscounted future payments into five years plus thereafter, reconcile to the discounted liability through imputed interest, with runnable Python.
The maturity analysis is the one disclosure that tells a statement reader when the cash goes out. Every other lease number is discounted, netted, or averaged; this table shows the raw, undiscounted future lease payments laid out year by year, and then bridges them back to the discounted liability the balance sheet actually carries. That bridge — a single reconciling line for imputed interest — is where the table either ties to the primary statements or fails review. This page treats the maturity analysis as a deterministic function: it takes the future payment vectors of a portfolio of leases, buckets them into Year 1 through Year 5 and a single "thereafter" line, and emits a table whose undiscounted total, less imputed interest, equals the reported lease liability to the cent. It carries both the compliance layer (which payments belong in the buckets, how the reconciliation must be shown, how ASC 842 and IFRS 16 differ) and the code layer (correct bucket boundaries, discounted-versus-undiscounted discipline, Decimal precision). It is one topic within the broader lease disclosure and financial statement reporting section, and it consumes the future payment column that liability amortization and schedule generation already produces.
Standard References Governing the Maturity Analysis Link to this section
Both frameworks require an undiscounted maturity analysis of lease liabilities, and both require it to reconcile to the discounted amount on the balance sheet — but they frame the requirement differently.
- ASC 842-20-50-6 requires the lessee to disclose a maturity analysis of its lease liabilities, showing the undiscounted cash flows on an annual basis for a minimum of each of the first five years and a total of the amounts for the remaining years, and to provide a reconciliation of the undiscounted cash flows to the lease liabilities recognized in the statement of financial position. The analysis is presented separately for finance leases and operating leases. The reconciling item is the imputed interest that discounting removes.
- IFRS 16.58 requires the lessee to disclose a maturity analysis of lease liabilities applying paragraphs 39 and B11 of IFRS 7, Financial Instruments: Disclosures, and to present it separately from the maturity analyses of other financial liabilities. IFRS 7.B11 lets the entity choose time bands appropriate to the maturities, but a banding of one year, two-to-five years, and beyond five years is common; the amounts are the undiscounted contractual cash flows.
- ASC 842-20-45-1 and IFRS 16.47 govern the balance-sheet amount the analysis reconciles to — the discounted lease liability measured under present value calculation logic and carried forward by the effective-interest unwind.
The consequence for a reporting engine is that the maturity analysis is not a fresh forecast: its payments are exactly the future payments already inside each lease's amortization schedule, and its reconciling line is exactly the remaining interest that schedule will accrue. The only genuinely new logic is bucketing — assigning each future payment to a disclosure year measured from the reporting date — and the reconciliation arithmetic. Payments that never entered the liability, such as usage-based variable and short-term costs, are excluded, and the rate used to derive imputed interest is the same locked rate the weighted-average disclosures report.
Input / Output Specification Link to this section
Pin the function's contract before writing arithmetic, so the bucketing rule and the reconciliation are explicit and testable.
| Field | Direction | Type | Validation rule | Notes |
|---|---|---|---|---|
| lease_id | in | string | non-empty, unique | Keys the reconciliation audit trail |
| reporting_date | in | date | fixed period end | Origin for every bucket offset |
| future_payments | in | list[(date, Decimal)] | dates strictly after reporting_date | Undiscounted liability payments only |
| remaining_liability | in | Decimal | equals discounted PV of future_payments | The balance-sheet closing balance |
| remaining_interest | in | Decimal | equals schedule future interest sum | Seeds the imputed-interest line |
| lease_class | in | enum | finance or operating | Drives the ASC 842 split |
| year_1 to year_5 | out | Decimal | each 2 dp, non-negative | Undiscounted totals per disclosure year |
| thereafter | out | Decimal | 2 dp, non-negative | Sum of payments beyond year five |
| imputed_interest | out | Decimal | 2 dp | Reconciling line: undiscounted less liability |
| reconciles | out | bool | must be true | Undiscounted total less imputed equals liability |
Two validation rules prevent the most common defects: rejecting any payment dated on or before the reporting date stops a past-due amount from landing in Year 1, and asserting that remaining_liability equals the discounted present value of future_payments stops a maturity table from being built on payments that do not match the liability it is supposed to reconcile to.
Formula Block: The Undiscounted-to-Discounted Reconciliation Link to this section
Each future payment is assigned to a disclosure bucket by the whole number of years between the reporting date and the payment date. For a payment falling
so a payment zero-to-one year out lands in bucket 1, one-to-two years out in bucket 2, and so on. Buckets 1 through 5 are reported individually; every bucket with
where
where
with
Step-by-Step Python Implementation Link to this section
The following module builds one lease's maturity analysis and reconciles it. Each step maps back to the formula block. Money is decimal.Decimal throughout; the reconciliation is asserted, not assumed.
Step 1 — Model inputs and pin precision. Represent every payment and the liability as Decimal, and raise the context precision so the discounted sum does not lose cents.
from dataclasses import dataclass
from decimal import Decimal, ROUND_HALF_UP, getcontext
from datetime import date
getcontext().prec = 28
CENT = Decimal("0.01")
def q(x: Decimal) -> Decimal:
return x.quantize(CENT, rounding=ROUND_HALF_UP)
@dataclass(frozen=True)
class MaturityInput:
reporting_date: date
future_payments: tuple[tuple[date, Decimal], ...] # undiscounted, after reporting_date
remaining_liability: Decimal # discounted balance-sheet amount
Step 2 — Bucket each payment by whole years from the reporting date. The bucket index is the number of complete years between the reporting date and the payment date, plus one, so a payment inside the first twelve months lands in Year 1. Buckets beyond five collapse into "thereafter".
def bucket_index(reporting_date: date, pay_date: date) -> int:
"""Whole years from reporting date to payment, 1-based (ASC 842-20-50-6)."""
if pay_date <= reporting_date:
raise ValueError(f"payment {pay_date} is not after the reporting date")
days = (pay_date - reporting_date).days
return days // 365 + 1 # 0-364 days -> 1, 365-729 days -> 2, ...
def bucket_payments(inp: MaturityInput) -> dict[str, Decimal]:
"""Fold undiscounted payments into Year 1..5 and Thereafter."""
buckets = {f"year_{y}": Decimal("0") for y in range(1, 6)}
buckets["thereafter"] = Decimal("0")
for pay_date, amount in inp.future_payments:
b = bucket_index(inp.reporting_date, pay_date)
key = f"year_{b}" if b <= 5 else "thereafter"
buckets[key] += amount
return {k: q(v) for k, v in buckets.items()}
Step 3 — Derive imputed interest and reconcile. Imputed interest is the residual between the undiscounted total and the discounted liability. The reconciliation must hold to the cent, so it is asserted.
def maturity_analysis(inp: MaturityInput) -> dict:
buckets = bucket_payments(inp)
undiscounted_total = sum(buckets.values(), Decimal("0"))
imputed_interest = q(undiscounted_total - inp.remaining_liability)
reconciled = q(undiscounted_total) - imputed_interest == q(inp.remaining_liability)
return {
**buckets,
"undiscounted_total": q(undiscounted_total),
"imputed_interest": imputed_interest, # reconciling line
"lease_liability": q(inp.remaining_liability),
"reconciled": reconciled,
}
Step 4 — Run and assert the reconciliation. A terminal assertion proves the undiscounted total less imputed interest equals the reported liability — the disclosure's audit control in code form.
if __name__ == "__main__":
rd = date(2026, 12, 31)
pays = tuple((date(2026 + (m - 1) // 12 + 1, ((m - 1) % 12) + 1, 1),
Decimal("2000.00")) for m in range(1, 73)) # 72 monthly payments
table = maturity_analysis(MaturityInput(rd, pays, Decimal("129384.00")))
assert table["reconciled"], "maturity analysis does not tie to the liability"
check = table["undiscounted_total"] - table["imputed_interest"]
assert check == table["lease_liability"] # undiscounted − imputed = liability
print("Year 1:", table["year_1"], " Thereafter:", table["thereafter"])
print("Imputed interest:", table["imputed_interest"])
The remaining_liability passed in must be the genuine discounted balance carried forward by the amortization engine; feeding a stale or independently estimated figure lets the reconciliation "pass" against the wrong target. In production the liability and the future payment vector should come from the same schedule object so they cannot drift apart.
Debugging and Precision Gotchas Link to this section
The maturity analysis fails in a small, recognizable set of ways. Each has a deterministic fix.
- Bucket boundary off-by-one. Placing a payment exactly one year out into Year 1 instead of Year 2, or into Year 2 instead of Year 1, is the classic defect. Anchor the bucket to whole years elapsed from the reporting date —
days // 365 + 1— and test the boundary cases explicitly: a payment at day 364 belongs to Year 1, and a payment at day 365 belongs to Year 2. Never derive the bucket from the payment's calendar year, which silently splits the first twelve months across two buckets when the reporting date is not 31 December. - Discounted-versus-undiscounted mixup. The single most damaging error is bucketing discounted payments while labelling the table undiscounted, or reconciling to a sum of discounted payments instead of the balance-sheet liability. The buckets must hold undiscounted contractual amounts; only the reconciling line and the liability are on a present-value basis. If the undiscounted total is suspiciously close to the liability, imputed interest has been double-removed.
- Reconciling to the wrong liability. The imputed-interest residual will always make the arithmetic "tie" against whatever liability you pass in, so passing a stale or estimated balance produces a table that reconciles to nothing real. Source the liability from the same schedule object that produced the payments.
- Leaking excluded payments into buckets. Usage-based variable payments and short-term-exempt payments never entered the liability, so bucketing them inflates the undiscounted total and breaks the reconciliation. Filter them upstream, in payment schedule data normalization, before the vector reaches this function.
- Rounding each bucket before summing the total. Quantizing each bucket and then summing, versus summing then quantizing, can differ by a cent and push the reconciliation off. Keep the accumulator in full precision and quantize the presented figures once.
Compliance Checklist — Before You Publish the Maturity Table Link to this section
Frequently Asked Questions Link to this section
How many buckets does the maturity analysis need?
Under ASC 842-20-50-6, at least six lines: each of the first five years individually and a single total for all remaining years ("thereafter"). IFRS 16.58 applies the IFRS 7 banding rules and permits time bands appropriate to the maturities, but disclosing years one through five plus a beyond-five total is common and comparable to the ASC 842 layout. In both cases the buckets hold undiscounted amounts and are followed by a reconciliation to the discounted liability.
What is the reconciling line between the maturity analysis and the balance sheet?
Imputed interest — the difference between the total undiscounted future payments and the discounted lease liability. It equals the financing cost the effective-interest method will accrue over the remaining term, and it is not an independent estimate: it is the sum of the future interest column of the amortization schedule. Subtracting it from the undiscounted total gives the present-value liability reported on the balance sheet (ASC 842-20-50-6).
Should the buckets be discounted?
No. The buckets show undiscounted contractual cash flows so that a reader can assess the timing and amount of future cash outflows. Discounting is applied only to derive the single reconciling line and the balance-sheet liability. Bucketing discounted payments, or reconciling to a sum of discounted amounts, is the most common maturity-analysis error and makes the imputed-interest line meaningless.
Do finance and operating leases share one maturity table under ASC 842?
No. ASC 842-20-50-6 requires the maturity analysis to be presented separately for finance leases and operating leases, each reconciled to its own liability line. Group the portfolio by class before bucketing and emit two tables. IFRS 16 has no operating class, so it presents a single lessee maturity analysis, kept separate only from the maturity analyses of other financial liabilities.
Related Link to this section
- Building a lease maturity analysis table in pandas — a focused how-to that buckets a portfolio and adds the imputed-interest reconciliation row with Decimal-backed sums.
- Weighted-average discount rate and term disclosures — the companion disclosure using the same locked rate that derives this table's imputed interest.
- Lease liability rollforward and reconciliation — reconciles the liability this maturity table ties to across the period.
- Calculating lease liability interest using the effective interest method — the interest allocation whose remaining total is exactly this table's imputed interest.
- Up: Lease Disclosure and Financial Statement Reporting