Skip to content

Correct TV licence liability and free licence value - #2147

Merged
MaxGhenis merged 3 commits into
mainfrom
fix-tv-licence-liability-2145
Oct 20, 2026
Merged

MaxGhenis merged 3 commits into
mainfrom
fix-tv-licence-liability-2145

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2145
Fixes #2149

What and why

PolicyEngine UK 2.106.1 used television ownership as the condition for both
the licence charge and the value of a free licence. Communications Act 2003
section 363 and the 2016 amendment to section 368 instead base liability on
using a receiver for live television or BBC on-demand programmes, regardless
of the device.

The free-licence value also declared a dimensionless unit even though it
returns the annual licence fee and enters GBP-denominated HBAI household
income. DWP's FYE 2025 HBAI methodology includes the cash value of complete
age-based TV licence exemptions, including the proportional value of any
partial-year exemption; it does not include the 50% blind-person discount as
a free licence.

This PR:

  • adds tv_licence_required, which can be supplied directly for households
    that use licensable services;
  • retains household_owns_tv as a documented fallback so existing datasets and
    household inputs keep their current behavior;
  • routes both tv_licence and free_tv_licence_value through the new variable;
  • separates the share covered by the complete age-based exemption from the
    blind discount, applies the blind discount only to the remaining paid share,
    and declares free_tv_licence_value in GBP;
  • adds the statutory and DWP methodology sources to the affected variables; and
  • adds cases for the liability conditions, the compatibility fallback, the
    full free-licence value, the 50% discount, and the variable's currency unit.

The implementation cites:

would_evade_tv_licence_fee remains a behavioural input rather than an
official policy rule.

#2152 has merged. This branch now includes that implementation from main:
tv_licence_free_licence_share uses fiscal_year_segments to apply the
Boolean Pension Credit requirement from 1 August 2020. The blind discount
applies only to the remaining paid share; the HBAI free-licence value excludes
the blind discount. #2153 is the dependent benefit-unit eligibility correction
and should merge after this PR.

Test plan

Rebased onto main e01f2f5ec882ecb22ec00d137b39ac70213aa58e on 12 October 2026.

  • 27 YAML cases passed across the free-licence value, licence liability and discount files, including two new cases that calculate the 2020 free/paid split without overriding the helper.
  • 5 Python tests passed across TV licence metadata and the Boolean/date-transition tests, including the year-keyed minimum-age reform.
  • Formatting, lint, Towncrier draft, fix-evidence validation and whitespace checks passed.
  • Retained changelog.d/2145.fixed.md.

Reproducer: policyengine_uk/tests/policy/baseline/gov/dcms/bbc/tv-licence/free_tv_licence_value.yaml::Case 9, the blind discount reduces only the paid part of the 2020 licence.

Only focused tests ran, sequentially, using the frozen-lockfile environment. No full suite or CI polling.

Impact (UK hub, 2026-10-19): none on the certified data.

  • The run: Aggregate impact run 39316716302 compares main e2ea32893 with main merged with this head (65ceae71e), on certified policyengine-uk-data 1.58.1, fiscal years 2025-26 to 2031-32.
  • The result: no aggregate changes in any year, HBAI-based poverty and deciles included. No check is broken; "VAT is never negative" fails on both sides, as on main.
  • Why, per an independent review of the code:
    • uk-data's build code never writes is_blind. The review read the source, not the file.
    • tv_licence_required falls back to household_owns_tv, the only liability input uk-data writes.
    • From 2021-22 the free share is 0 or 1, where the new discount equals the old maximum.
  • Hand-entered blind households do change: in 2026-27, one with no age concession loses £90 of HBAI income in kind. Its licence charge stays £90.

axiom: TheAxiomFoundation/rulespec-uk#473 queued

@anth-volk anth-volk changed the title Base TV licence liability on licensable viewing Correct TV licence liability and free licence value Oct 5, 2026
@anth-volk
anth-volk marked this pull request as ready for review October 6, 2026 21:58
@anth-volk
anth-volk requested a review from vahid-ahmadi October 6, 2026 21:58
@anth-volk

Copy link
Copy Markdown
Collaborator Author

@vahid-ahmadi, could you please review this PR with particular attention to two design choices? First, do you have concerns about adding tv_licence_required as an overridable household Boolean with household_owns_tv as the compatibility fallback, including its public-input and dataset implications? Second, does retaining would_evade_tv_licence_fee as the existing household-level behavioural Boolean—rather than separating an assumed evasion rate from each household’s realised evasion status—create conceptual or implementation concerns? Please let us know if you would prefer a different representation for either.

@vahid-ahmadi

Copy link
Copy Markdown
Collaborator

Review pass (Claude Code, on Vahid's behalf) at cb37b9ac

Short version: approve with one should-fix. The should-fix is about how this PR combines with #2152, not about this PR alone. Liability now follows the law (s. 363, as amended in 2016): the trigger is watching live or using iPlayer, not owning a TV. The free-licence value is in GBP and counts only a complete exemption, which matches DWP's HBAI method.

The new tests fail on main and pass here: 6 of the 12 new YAML cases fail on main. The DCMS suite (17) and the metadata test pass. CI is 6/6 green, and the PR merges cleanly with main.

Your two questions

  1. tv_licence_required as an overridable household Boolean, falling back to household_owns_tv: fine. That's the repository's usual input-with-default pattern.
    • Datasets that don't supply it keep their current results, because they fall back to the stochastic ownership draw.
    • Household-calculator users still default to True, since household_owns_tv defaults to True.
    • One thing worth a sentence in the variable's documentation: ownership is a two-sided proxy. It overstates liability for owners who only watch on demand outside iPlayer, and understates it for people with no TV who watch live on a laptop or phone.
  2. Keeping would_evade_tv_licence_fee as a realised household Boolean: no concern. It follows the repository's take-up pattern: a stored draw, with the rate kept separately in evasion_rate.yaml for the data build.
    • The one consequence to state is that evasion doesn't respond to a reform, for example a fee change. That's an assumption, not a bug.
    • Separating rate from realisation inside the engine would only pay off if someone wants evasion to respond to the fee, which needs an elasticity first.

Findings

  1. Should-fix: with Correct the 2020 over-75 TV licence transition #2152 merged, the 2020-21 value of the free licence disappears for over-75s not on Pension Credit. I merged Correct the 2020 over-75 TV licence transition #2152 into this branch locally and ran an 80-year-old with £40,000 of private pension (so no Pension Credit) in 2020:

    • tv_licence_discount = 0.321: free for the 117 days to 31 July 2020.
    • tv_licence = £107.01, which is correct.
    • free_tv_licence_value = £0, because discount == 1 is false. On main, and on Correct the 2020 over-75 TV licence transition #2152 alone, it's 0.321 × £157.50 = £50.49.

    So HBAI income drops the 117 days of free licence those households actually had.

    • Fix: value the free licence as the fee times the share of the year with the complete age exemption, and leave out the blind discount. That means splitting the age component out of tv_licence_discount, for example as a tv_licence_free_licence_share variable that both formulas read, rather than testing the combined discount for equality with 1.
    • Pension Credit recipients and every year from 2021 are unaffected.
  2. Nit, mostly Correct the 2020 over-75 TV licence transition #2152's: blind and over 75 in 2020-21. max(aged_discount, blind_discount) takes the larger of two annualised values: max(0.321, 0.5) = 0.5, so the charge is £78.75. Applied by segment, the household is free for 117 days and pays half for 248, so £53.50. It's an edge case, but the share variable in item 1 would fix both.

  3. Nit: discount == 1 is an exact float comparison. It's safe with the parameter at 1.0, but it becomes fragile once fractional segment weights exist. The fix in item 1 removes it.

  4. Nit: the PR fixes Correct the free TV licence value's liability trigger and currency treatment #2149 as well as Base TV licence liability on licensable viewing rather than television ownership #2145, but has one changelog fragment. Fine if the fragment covers both, which it does in substance.

Checked and correct

  • The fee history in colour.yaml, which this PR leaves unchanged: £147 (2017), £150.50 (2018), £154.50 (2019), £157.50 (2020), £159 (frozen 2021 to 2023), £169.50 (2024), £174.50 (2025), £180 (2026). It matches the regulations the PR cites, including SI 2026/104.
  • Probes on this branch, 2019 to 2026:
    • Over-75s on Pension Credit get the full fee as free-licence value and a £0 charge.
    • From August 2020, over-75s not on Pension Credit pay the full fee.
    • Blind households pay half and get £0 free-licence value, since DWP doesn't count the blind discount.
    • Under-75s pay the full fee.
  • free_tv_licence_value feeds only hbai_household_net_income. The charge feeds household_tax and gov_tax.
  • The care home (ARC) £7.50 concession isn't modelled before or after this PR. That's out of scope.

Merge order with #2152 and #2153

There are no textual conflicts: #2147 and #2152 touch different files. But they interact through tv_licence_discount, so whichever merges second needs item 1 resolved. My suggested order:

  1. Correct the 2020 over-75 TV licence transition #2152, with its preserve_calendar_dates fix;
  2. this PR, with item 1 applied on top;
  3. Limit free TV licences to recipients and partners #2153, which is stacked on Correct the 2020 over-75 TV licence transition #2152.

@anth-volk
anth-volk force-pushed the fix-tv-licence-liability-2145 branch from cb37b9a to a68b2e9 Compare October 7, 2026 12:05
@anth-volk

Copy link
Copy Markdown
Collaborator Author

Implemented the requested review changes in a68b2e9:

  • added tv_licence_free_licence_share for the complete age-based exemption, separate from the blind discount;
  • made the combined discount apply the blind discount only to the share of the year not already free;
  • made free_tv_licence_value use the complete-exemption share directly, removing the exact float comparison;
  • documented the two directions in which TV ownership can misclassify liability and that realised evasion does not respond to fee reforms;
  • added coverage for fractional free-licence value, the blind-plus-fractional-free-period case, and eligibility with and without Pension Credit.

For the reviewed 2020 example, a free share of 117/365 plus the blind discount now produces a TV licence charge of 53.51 and a free-licence value of 50.49. PR #2152 can now calculate that share from the dated transition.

Focused validation after rebasing onto current main:

  • make format: passed
  • TV licence policy directory: 16 passed
  • TV licence metadata test: 1 passed
  • git diff --check: passed

The full UK test suite was not run.

@vahid-ahmadi

Copy link
Copy Markdown
Collaborator

Thanks Anthony, the share split is right. free_tv_licence_value now counts only the age exemption, the blind discount only applies to the part of the year that isn't already free, and the float comparison is gone. The TV licence tests pass merged with main.

Now that #2152 is in, this needs a rebase: both PRs change tv_licence_discount.py, so it conflicts. The resolution I tested is your version of tv_licence_discount.py, with #2152's fiscal_year_segments loop moved into tv_licence_free_licence_share. With that, an 80-year-old without Pension Credit in 2020 pays £107.01 and gets £50.49 of free-licence value, a blind one pays £53.51, a year-keyed min_age reform stays in its own year, and the full YAML suite passes. Happy to approve once that's pushed.

@MaxGhenis

Copy link
Copy Markdown
Collaborator

Suggested Reproducer: line for the new Fix evidence check; add it to the description, or narrow it to the test that reproduces the bug. It lists the tests this PR adds or changes, and the check (.github/check_fix_pr.py) accepts it against this PR's diff.

Reproducer: `policyengine_uk/tests/policy/baseline/gov/dcms/bbc/tv-licence/free_tv_licence_value.yaml`, `policyengine_uk/tests/policy/baseline/gov/dcms/bbc/tv-licence/tv_licence.yaml`, `policyengine_uk/tests/test_tv_licence_metadata.py::test_free_tv_licence_value_uses_currency_units`

The check isn't required yet (decision d1368); it fails fix PRs whose description names no reproducer.

🤖 Generated with Claude Code

@anth-volk
anth-volk force-pushed the fix-tv-licence-liability-2145 branch from a68b2e9 to a99deaa Compare October 12, 2026 13:30
@anth-volk

Copy link
Copy Markdown
Collaborator Author

Rebased onto main e01f2f5ec and incorporated merged #2152 as requested. The fiscal_year_segments calculation now lives in tv_licence_free_licence_share; must_claim_pc remains Boolean with its actual 1 August 2020 start date.

The blind discount applies only to the remaining paid share. The two new 2020 cases calculate the exemption rather than supplying its result: £50.49 free-licence value and £107.01 paid, or £53.51 paid with blindness. The blind discount does not enter the free HBAI value.

Validation: 27 YAML cases and 5 Python tests passed, sequentially, including the year-keyed age-reform test. Formatting, lint, Towncrier draft, fix-evidence validation and whitespace checks passed. No full suite or CI polling. #2153 will use this branch as its base and should merge afterward.

@vahid-ahmadi vahid-ahmadi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Anthony. Re-checked after the rebase at a99deaa: the resolution is the one I tested, with #2152's fiscal_year_segments loop now in tv_licence_free_licence_share and the blind discount applied only to the paid share. Cases 8 and 9 give £50.49 free value with £107.01 or £53.51 paid, the TV licence tests pass locally, so does the full YAML suite (3,071), and CI is green. Approving.

@MaxGhenis

Copy link
Copy Markdown
Collaborator

UK hub: an independent review at a99deaa5f (Opus 5.5, code reading; it ran no tests) gave APPROVE WITH NITS. Max's d1728 has the hub merge this on its gates; these nits are for a follow-up, not blockers. @anth-volk, FYI.

Confirmed:

Nits:

  1. The body overstates DWP. HBAI FYE 2025 counts as income in kind only "free TV licences for those aged 75 and over who receive Pension Credit". It says nothing about part-year exemptions or the blind discount. Excluding the blind discount follows from that list, but part-year valuation is our modelling choice. free_tv_licence_value.py:6-9 states the rule correctly; only the body's attribution is off.
  2. The blind 50% is statutory: SI 2004/692 reg 5 ("Concessions for blind persons"). Nothing cites it. tv_licence_required.py:16-19 also omits SI 2004/692 reg 9 (the receiver definition), and its docstring omits that installing a receiver for live TV also needs a licence (s.363(1)).
  3. The age-discount rate also multiplies the free share (tv_licence_free_licence_share.py:32). The rate is 1 at current law, so nothing changes today. Under a reform below 1, though, the share becomes a rate, stacks with the blind discount (0.5 + 0.5 × 0.5 = 0.75, where main gave 0.5), and counts in HBAI income. Document it or rename the variable.
  4. Stale route text: data/fiscal_partition.yaml:289,292 still reads tv_licence -> tv_licence_discount -> pension_credit. It is documentation only.
  5. Tests to add:
    • a fallback case and an evasion case for free_tv_licence_value;
    • property tests: share in [0,1]; share ≤ discount ≤ 1; charge + free value + blind saving = fee; tv_licence_required == household_owns_tv when not supplied.
  6. Limit free TV licences to recipients and partners #2153 is stacked on this branch. Retarget it to main after this merges.

The impact (none on the certified data) and the axiom: line (rulespec-uk#473, now corrected to cite reg 5) are in the body.

🤖 Generated with Claude Code

@MaxGhenis
MaxGhenis added this pull request to the merge queue Oct 19, 2026
Merged via the queue into main with commit 88056f5 Oct 20, 2026
16 of 17 checks passed
@MaxGhenis
MaxGhenis deleted the fix-tv-licence-liability-2145 branch October 20, 2026 08:36
@MaxGhenis

Copy link
Copy Markdown
Collaborator

Merged by the UK hub (session local_8e19f9d5, Opus 5.5) on Max's d1728 (2026-10-19): the hub merges #2458, #2497 and #2147 on its gates, after correcting each body's axiom: line to a value the parity rule accepts. One merge-queue enqueue, pinned to head a99deaa5f787a16175296db0814a7b35e4068c76.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants