Skip to content

Name the offending key when per_key_traits rejects a value - #978

Open
sap1110 wants to merge 1 commit into
ipython:mainfrom
sap1110:dict-per-key-traits-error-names-key
Open

Name the offending key when per_key_traits rejects a value#978
sap1110 wants to merge 1 commit into
ipython:mainfrom
sap1110:dict-per-key-traits-error-names-key

Conversation

@sap1110

@sap1110 sap1110 commented Aug 17, 2026

Copy link
Copy Markdown

Closes #920

A Dict configured with per_key_traits reports failures against the trait as a whole, so the message names the trait but never the key that was actually invalid:

class Foo(HasTraits):
    bar = Dict(per_key_traits={"this": Unicode(), "that": Dict()})

Foo().bar = {"this": "valid", "that": False}
TraitError: Values of the 'bar' trait of a Foo instance must be a dict, but a
value of False <class 'bool'> was specified.

With more than one per-key trait configured, that leaves the caller to work out which key the message is about. Failures routed through a per_key_traits entry now name it:

TraitError: Value at key 'that' of the 'bar' trait of a Foo instance must be a
dict, but a value of False <class 'bool'> was specified.

Only the per-key path changes: it goes through a new per_key_element_error rather than element_error, so a uniform value_trait failure still reports against the trait as a whole. There's a test asserting that, since the wording of these messages is the sort of thing downstream test suites match on.

Not addressed here

The issue also mentions nested dicts. Those are still misreported, and the cause is separate from the key naming:

bar = Dict(per_key_traits={"that": Dict(per_key_traits={"that": Unicode()})})
Foo().bar = {"that": {"that": 3}}
# Value at key 'that' ... must be a dict, but a value of {'that': 3} <class 'dict'> was specified.

The value is a dict. The inner trait raises an accurate error, and the outer validate_elements discards it and substitutes its own. TraitType.error already has a chaining protocol for exactly this (child errors carrying (value, info, *traits)), which Dict.validate_elements bypasses.

Routing per-key failures through that protocol would fix the nested case, but it changes a shared error contract and the composed message shape, so it seemed worth agreeing on separately rather than folding into this one. Happy to do it in a follow-up if you'd like it, in whichever direction you prefer.


🤖🍆 Prepared with the help of an AI coding agent, marked as the contributing guide asks agents to do. Reviewed and tested by me.

A Dict configured with per_key_traits reported failures against the trait as a
whole, so the message named the trait but not the key that was actually invalid:

    Values of the 'bar' trait of a Foo instance must be a dict, but a value of
    False <class 'bool'> was specified.

With several per-key traits configured, that leaves the caller to work out which
key the message is about. Failures routed through a per_key_traits entry now
name it:

    Value at key 'that' of the 'bar' trait of a Foo instance must be a dict, but
    a value of False <class 'bool'> was specified.

Only the per-key path changes. A uniform value_trait failure still reports
against the trait as a whole, and there is a test covering that.
@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.16%. Comparing base (80756bf) to head (1a42733).

Components Coverage Δ
traitlets 85.52% <100.00%> (+0.01%) ⬆️
tests 99.07% <100.00%> (+<0.01%) ⬆️

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

traitlets Dict with per_key_traits should be more specific in a TraitError

1 participant