Skip to content

BUG: Detect a duplicate dictionary key whose first value is falsy - #4024

Merged
stefan6419846 merged 1 commit into
py-pdf:mainfrom
devYRPauli:fix/duplicate-key-falsy-first-value
Aug 27, 2026
Merged

stefan6419846 merged 1 commit into
py-pdf:mainfrom
devYRPauli:fix/duplicate-key-falsy-first-value

Conversation

@devYRPauli

Copy link
Copy Markdown
Contributor

Problem

DictionaryObject.read_from_stream() decides whether a key has already been seen with:

if not data.get(key):
    data[key] = value
else:
    # multiple definitions of key not permitted

The test checks truthiness, not presence. A first value of NumberObject(0) is falsy. On the second occurrence data.get(key) returns 0, so not 0 is True. The branch then treats the key as new. It does not report the duplicate, and the second value replaces the first.

This happens even with strict=True, where the reader is supposed to raise.

Behaviour on current main:

input result
<< /Count 7 /Count 5 >> warns, keeps 7
<< /Count 0 /Count 5 >> silent, keeps 5

The second row is the bug. pypdf does not raise, and it does not log. It also keeps the opposite value from every other duplicate.

Change

if key not in data:

Presence, not truthiness. The falsy case now behaves like every other duplicate: strict=True raises, non-strict warns, and the first value is kept.

input before after
<< /Count 7 /Count 5 >> warns, keeps 7 unchanged
<< /Count 0 /Count 5 >> silent, keeps 5 warns, keeps 0
<< /Count 0 /Size 5 >> both keys kept unchanged

The non-strict result for the second row changes from 5 to 0. That change is the point. It now matches the first row, which has always kept the first value.

Tests

Four tests in tests/generic/test_data_structures.py, all from bytes in memory, no fixture download:

  • duplicate key with a falsy first value raises under strict=True
  • the same input under non-strict logs the message and keeps the first value
  • duplicate key with a non-falsy first value still raises, unchanged
  • two different keys where the first value is 0 parse normally, so there is no false positive

Reverting the one-line change fails the first two and leaves the last two passing.

tests/generic/ reports 90 passed with the change and 86 without it. One test, test_files.py::test_embedded_file__basic, fails either way in my environment because a fixture is missing locally.

ruff check passes on both files.

AI usage disclosure

Per the AI policy in CONTRIBUTING.md: I used Claude Code (Anthropic, Opus 5) while working on this. I reviewed the change, reproduced the behaviour myself before and after, and understand and stand behind it. Happy to explain any part of it or adjust the approach.

read_from_stream() used "if not data.get(key)" to decide whether a key had
already been seen. A first value of NumberObject(0) is falsy, so the second
occurrence looked like a new key. The duplicate went unreported and the second
value replaced the first, even with strict=True.

Checking "key not in data" makes the falsy case behave like every other
duplicate: strict raises, non-strict warns, and the first value is kept.
@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.00%. Comparing base (2955949) to head (706209c).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4024   +/-   ##
=======================================
  Coverage   98.00%   98.00%           
=======================================
  Files          57       57           
  Lines       11128    11128           
  Branches     2087     2087           
=======================================
  Hits        10906    10906           
  Misses        124      124           
  Partials       98       98           

☔ 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.

@stefan6419846 stefan6419846 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. I do not know why #69 implemented it like this, but your change seems reasonable.

@stefan6419846
stefan6419846 merged commit a1f510c into py-pdf:main Aug 27, 2026
20 checks passed
stefan6419846 added a commit that referenced this pull request Sep 4, 2026
## What's new

### Security (SEC)
- Limit value for Roman numerals (#4047) by @stefan6419846

### New Features (ENH)
- _cmap.py: Also parse encoding for embedded CFF Type1 fonts (#4032) by @PJBrs

### Performance Improvements (PI)
- Cache repeated text extraction character lookups (#4036) by @petermik68-sudo

### Bug Fixes (BUG)
- Treat an empty /Filter array as no filter when extracting images (#4026) by @Anai-Guo
- Detect a duplicate dictionary key whose first value is falsy (#4024) by @devYRPauli
- Make is_open=False collapse outline items (#3998) by @SomSamantray

### Robustness (ROB)
- Multiple changes for wrong inputs by @RavSinghChandan
- Skip trailing duplicate %%EOF markers when locating startxref (#4015) by @Anai-Guo
- Do not crash on a non-array destination (#3976) by @eeshsaxena
- Handle annotations without subtype during merge (#3999) by @Nexlu1

### Documentation (DOC)
- Use AnnotationFlag enum instead of plain integers (#3997) by @RavSinghChandan

### Code Style (STY)
- Multiple small changes detected from test runs by @RavSinghChandan

[Full Changelog](6.16.2...6.17.0)
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.

2 participants