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 years after the reporting date, the bucket index is

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 collapses into the single "thereafter" line. The undiscounted bucket totals are

where is the undiscounted payment in period . The reconciliation that ties the table to the balance sheet is the imputed-interest bridge:

where is the discounted lease liability on the balance sheet and the imputed interest is the residual

with the periodic discount rate and the number of remaining periods. The second identity is the important one: imputed interest is not estimated independently — it is the total of the future interest column the effective-interest method allocates over the remaining life. If the imputed-interest line you print does not equal the remaining schedule interest, the table is not reconciled.

Bucketing future payments and reconciling to the discounted liability Undiscounted future lease payments are sorted along a timeline from the reporting date into six columns: Year 1, Year 2, Year 3, Year 4, Year 5, and a wider Thereafter column that collects everything beyond year five. The heights of the bars represent the undiscounted totals in each bucket. Below, the sum of all buckets forms the undiscounted total; subtracting the imputed-interest reconciling line yields the discounted lease liability carried on the balance sheet. reporting date → Year 1 Year 2 Year 3 Year 4 Year 5 Thereafter all buckets > 5 Undiscounted total Σ buckets + thereafter − Imputed interest reconciling line = Discounted liability balance-sheet amount L Each payment is bucketed by whole years from the reporting date; buckets beyond five collapse into one line.
Undiscounted future payments are bucketed by whole years from the reporting date into Year 1 through Year 5 and a single Thereafter line. Summed, they give the undiscounted total; subtracting the imputed-interest reconciling line lands on the discounted liability the balance sheet carries.

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.

Decision flow for placing a cash flow in the maturity analysis A decision flow tests each contractual cash flow before it reaches the maturity table. First, did the payment enter the lease liability? Usage-based variable payments and short-term-exempt payments are diverted to period cost and never bucketed. Payments that did enter the liability are tested for timing: those on or before the reporting date are rejected as past due, and those after it are assigned a bucket by whole years from the reporting date, with buckets beyond five collapsed into the thereafter line. The bucketed totals then flow into the reconciliation, where subtracting imputed interest from the undiscounted total yields the discounted liability. Future cash flow In the liability? Period cost only variable · short-term After report date? Reject — past due not a future payment Bucket by whole years >5 → thereafter Reconciliation undiscounted − imputed = liability No Yes No Yes
Only payments that entered the liability and fall after the reporting date are bucketed; variable, short-term, and past-due amounts are diverted. Each surviving payment is placed by whole years from the reporting date, buckets beyond five collapse into thereafter, and the totals feed the imputed-interest reconciliation to the liability.

Debugging and Precision Gotchas Link to this section

The maturity analysis fails in a small, recognizable set of ways. Each has a deterministic fix.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Continue reading