Skip to content

Add type hints - #611

Merged
eliben merged 14 commits into
eliben:mainfrom
pmhahn:typing
May 27, 2026
Merged

Add type hints#611
eliben merged 14 commits into
eliben:mainfrom
pmhahn:typing

Conversation

@pmhahn

@pmhahn pmhahn commented Mar 19, 2025

Copy link
Copy Markdown
Contributor

This is the mayor PR to add Python type hints #514 – without #609 this will not be complete as elftools.construct.Container is used in many places, which is a container for Anything: retrieving values from it will be typed Any, which basically means untyped: without manually type-hinting every such use case those values do propagate further and even spill into the public API.

Because of missing 6f99ce0 running mypy will find the following errors:

elftools/dwarf/structs.py:581: error: "Container" has no attribute "first"  [attr-defined]
elftools/dwarf/structs.py:583: error: "Container" has no attribute "first"  [attr-defined]
elftools/dwarf/structs.py:585: error: "Container" has no attribute "first"  [attr-defined]
elftools/dwarf/structs.py:587: error: "Container" has no attribute "second"  [attr-defined]
elftools/dwarf/structs.py:590: error: "Container" has no attribute "first"  [attr-defined]
elftools/dwarf/callframe.py:99: error: "Container" has no attribute "length"  [attr-defined]
elftools/dwarf/ranges.py:41: error: "Container" has no attribute "start_index"  [attr-defined]
elftools/dwarf/ranges.py:42: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/ranges.py:42: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/ranges.py:42: error: "Container" has no attribute "length"  [attr-defined]
elftools/dwarf/ranges.py:46: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/ranges.py:46: error: "Container" has no attribute "address"  [attr-defined]
elftools/dwarf/ranges.py:47: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/ranges.py:47: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/ranges.py:47: error: "Container" has no attribute "start_offset"  [attr-defined]
elftools/dwarf/ranges.py:47: error: "Container" has no attribute "end_offset"  [attr-defined]
elftools/dwarf/ranges.py:48: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/ranges.py:48: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/ranges.py:48: error: "Container" has no attribute "start_address"  [attr-defined]
elftools/dwarf/ranges.py:48: error: "Container" has no attribute "end_address"  [attr-defined]
elftools/dwarf/ranges.py:49: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/ranges.py:49: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/ranges.py:49: error: "Container" has no attribute "start_address"  [attr-defined]
elftools/dwarf/ranges.py:49: error: "Container" has no attribute "length"  [attr-defined]
elftools/dwarf/ranges.py:50: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/ranges.py:50: error: "Container" has no attribute "index"  [attr-defined]
elftools/dwarf/ranges.py:51: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/ranges.py:51: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/ranges.py:51: error: "Container" has no attribute "start_index"  [attr-defined]
elftools/dwarf/ranges.py:51: error: "Container" has no attribute "end_index"  [attr-defined]
elftools/dwarf/ranges.py:70: error: "Container" has no attribute "version"  [attr-defined]
elftools/dwarf/ranges.py:180: error: "Container" has no attribute "offset_table_offset"  [attr-defined]
elftools/dwarf/ranges.py:180: error: "Container" has no attribute "is64"  [attr-defined]
elftools/dwarf/ranges.py:180: error: "Container" has no attribute "offset_count"  [attr-defined]
elftools/dwarf/ranges.py:181: error: "Container" has no attribute "offset_after_length"  [attr-defined]
elftools/dwarf/ranges.py:181: error: "Container" has no attribute "unit_length"  [attr-defined]
elftools/dwarf/ranges.py:188: error: "Container" has no attribute "entry_type"  [attr-defined]
elftools/dwarf/locationlists.py:53: error: "Container" has no attribute "start_index"  [attr-defined]
elftools/dwarf/locationlists.py:54: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/locationlists.py:54: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/locationlists.py:54: error: "Container" has no attribute "length"  [attr-defined]
elftools/dwarf/locationlists.py:54: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/locationlists.py:58: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/locationlists.py:58: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/locationlists.py:58: error: "Container" has no attribute "address"  [attr-defined]
elftools/dwarf/locationlists.py:59: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/locationlists.py:59: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/locationlists.py:59: error: "Container" has no attribute "start_offset"  [attr-defined]
elftools/dwarf/locationlists.py:59: error: "Container" has no attribute "end_offset"  [attr-defined]
elftools/dwarf/locationlists.py:59: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/locationlists.py:60: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/locationlists.py:60: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/locationlists.py:60: error: "Container" has no attribute "start_address"  [attr-defined]
elftools/dwarf/locationlists.py:60: error: "Container" has no attribute "length"  [attr-defined]
elftools/dwarf/locationlists.py:60: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/locationlists.py:61: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/locationlists.py:61: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/locationlists.py:61: error: "Container" has no attribute "start_address"  [attr-defined]
elftools/dwarf/locationlists.py:61: error: "Container" has no attribute "end_address"  [attr-defined]
elftools/dwarf/locationlists.py:61: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/locationlists.py:62: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/locationlists.py:62: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/locationlists.py:62: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/locationlists.py:63: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/locationlists.py:63: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/locationlists.py:63: error: "Container" has no attribute "index"  [attr-defined]
elftools/dwarf/locationlists.py:64: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/locationlists.py:64: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/locationlists.py:64: error: "Container" has no attribute "start_index"  [attr-defined]
elftools/dwarf/locationlists.py:64: error: "Container" has no attribute "end_index"  [attr-defined]
elftools/dwarf/locationlists.py:64: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/locationlists.py:81: error: "Container" has no attribute "version"  [attr-defined]
elftools/dwarf/locationlists.py:222: error: "Container" has no attribute "version"  [attr-defined]
elftools/dwarf/locationlists.py:286: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/locationlists.py:287: error: "Container" has no attribute "entry_end_offset"  [attr-defined]
elftools/dwarf/locationlists.py:288: error: "Container" has no attribute "entry_type"  [attr-defined]
elftools/dwarf/locationlists.py:290: error: "Container" has no attribute "address"  [attr-defined]
elftools/dwarf/locationlists.py:292: error: "Container" has no attribute "start_offset"  [attr-defined]
elftools/dwarf/locationlists.py:292: error: "Container" has no attribute "end_offset"  [attr-defined]
elftools/dwarf/locationlists.py:292: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/locationlists.py:294: error: "Container" has no attribute "start_address"  [attr-defined]
elftools/dwarf/locationlists.py:294: error: "Container" has no attribute "length"  [attr-defined]
elftools/dwarf/locationlists.py:294: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/locationlists.py:296: error: "Container" has no attribute "start_address"  [attr-defined]
elftools/dwarf/locationlists.py:296: error: "Container" has no attribute "end_address"  [attr-defined]
elftools/dwarf/locationlists.py:296: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/locationlists.py:298: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/dwarfinfo.py:478: error: "Container" has no attribute "address_size"  [attr-defined]
elftools/elf/structs.py:429: error: "Container" has no attribute "pr_datasz"  [attr-defined]
elftools/elf/structs.py:430: error: "Container" has no attribute "pr_datasz"  [attr-defined]
elftools/elf/structs.py:433: error: "Container" has no attribute "pr_type"  [attr-defined]
elftools/elf/structs.py:435: error: "Container" has no attribute "pr_type"  [attr-defined]
elftools/elf/structs.py:437: error: "Container" has no attribute "pr_type"  [attr-defined]
elftools/elf/structs.py:439: error: "Container" has no attribute "pr_type"  [attr-defined]
elftools/elf/structs.py:441: error: "Container" has no attribute "pr_type"  [attr-defined]
elftools/elf/structs.py:441: error: "Container" has no attribute "pr_datasz"  [attr-defined]
elftools/elf/notes.py:71: error: "Container" has no attribute "pr_datasz"  [attr-defined]
elftools/elf/dynamic.py:67: error: "Container" has no attribute "d_tag"  [attr-defined]
elftools/elf/dynamic.py:68: error: "Container" has no attribute "d_tag"  [attr-defined]
elftools/elf/dynamic.py:69: error: "Container" has no attribute "d_val"  [attr-defined]
elftools/elf/dynamic.py:77: error: "Container" has no attribute "d_tag"  [attr-defined]
elftools/elf/dynamic.py:80: error: "Container" has no attribute "d_tag"  [attr-defined]
elftools/elf/dynamic.py:81: error: "Container" has no attribute "d_tag"  [attr-defined]
elftools/elf/dynamic.py:83: error: "Container" has no attribute "d_ptr"  [attr-defined]
elftools/elf/dynamic.py:84: error: "Container" has no attribute "d_tag"  [attr-defined]
elftools/elf/dynamic.py:203: error: "Container" has no attribute "d_tag"  [attr-defined]
elftools/elf/elffile.py:152: error: "Container" has no attribute "sh_type"  [attr-defined]
elftools/elf/elffile.py:306: error: "Container" has no attribute "sh_offset"  [attr-defined]
elftools/elf/elffile.py:397: error: "Container" has no attribute "sh_offset"  [attr-defined]
elftools/elf/descriptions.py:59: error: "Container" has no attribute "d_val"  [attr-defined]
elftools/elf/descriptions.py:300: error: "Container" has no attribute "pr_type"  [attr-defined]
elftools/elf/descriptions.py:301: error: "Container" has no attribute "pr_data"  [attr-defined]
elftools/elf/descriptions.py:302: error: "Container" has no attribute "pr_datasz"  [attr-defined]

Similar missing db4fb21 is responsible for

elftools/elf/enums.py:37: error: Dict entry 2 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:64: error: Dict entry 22 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:76: error: Dict entry 7 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:278: error: Dict entry 187 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:324: error: Dict entry 30 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:393: error: Dict entry 5 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:421: error: Dict entry 14 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:454: error: Dict entry 8 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:473: error: Dict entry 14 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:485: error: Dict entry 7 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:489: error: Dict entry 0 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:497: error: Dict entry 3 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:585: error: Dict entry 83 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:748: error: Dict entry 51 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:795: error: Dict entry 43 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:838: error: Dict entry 39 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:848: error: Dict entry 6 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:951: error: Dict entry 98 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:1023: error: Dict entry 4 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:1042: error: Dict entry 5 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:1054: error: Dict entry 7 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:1065: error: Dict entry 6 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:1077: error: Dict entry 7 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:1085: error: Dict entry 4 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]

These I do not know how to fix - their type is not static and depends dynamically on the opened ELF file:

elftools/dwarf/ranges.py:50: error: Cannot determine type of "dwarfinfo"  [has-type]
elftools/dwarf/ranges.py:51: error: Cannot determine type of "dwarfinfo"  [has-type]
elftools/dwarf/locationlists.py:63: error: Cannot determine type of "dwarfinfo"  [has-type]
elftools/dwarf/locationlists.py:64: error: Cannot determine type of "dwarfinfo"  [has-type]
elftools/elf/notes.py:25: error: Cannot determine type of "structs"  [has-type]
elftools/elf/notes.py:30: error: Cannot determine type of "structs"  [has-type]
elftools/elf/notes.py:48: error: Cannot determine type of "structs"  [has-type]
elftools/elf/notes.py:56: error: Cannot determine type of "structs"  [has-type]
elftools/elf/notes.py:60: error: Cannot determine type of "structs"  [has-type]
elftools/elf/notes.py:70: error: Cannot determine type of "structs"  [has-type]
elftools/elf/sections.py:42: error: Cannot determine type of "structs"  [has-type]
elftools/elf/sections.py:180: error: Cannot determine type of "structs"  [has-type]
elftools/elf/relocation.py:63: error: Cannot determine type of "structs"  [has-type]
elftools/elf/relocation.py:131: error: Cannot determine type of "structs"  [has-type]
elftools/elf/relocation.py:185: error: Cannot determine type of "structs"  [has-type]
elftools/elf/relocation.py:345: error: Cannot determine type of "structs"  [has-type]
elftools/elf/relocation.py:347: error: Cannot determine type of "structs"  [has-type]
elftools/elf/relocation.py:349: error: Cannot determine type of "structs"  [has-type]
elftools/elf/relocation.py:351: error: Cannot determine type of "structs"  [has-type]
elftools/elf/hash.py:49: error: Cannot determine type of "structs"  [has-type]
elftools/elf/hash.py:115: error: Cannot determine type of "structs"  [has-type]
elftools/elf/hash.py:120: error: Cannot determine type of "structs"  [has-type]
elftools/elf/hash.py:121: error: Cannot determine type of "structs"  [has-type]
elftools/elf/gnuversions.py:153: error: Cannot determine type of "structs"  [has-type]
elftools/elf/gnuversions.py:196: error: Cannot determine type of "structs"  [has-type]
elftools/elf/dynamic.py:110: error: Cannot determine type of "structs"  [has-type]

And finally the last group of issues, which are also caused by missing cc7b1ea:

elftools/dwarf/structs.py:413: error: Unused "type: ignore" comment  [unused-ignore]
elftools/common/utils.py:41: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/common/utils.py:41: error: A function returning TypeVar should receive at least one argument containing the same TypeVar  [type-var]
elftools/elf/structs.py:54: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/elf/structs.py:55: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/elf/structs.py:56: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/elf/structs.py:57: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/elf/structs.py:58: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/elf/structs.py:59: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/elf/structs.py:60: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/elf/structs.py:61: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/elf/structs.py:62: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/dwarf/descriptions.py:93: error: Incompatible types in string interpolation (expression has type "int | None", placeholder has type "int")  [str-format]
elftools/dwarf/descriptions.py:135: error: Incompatible types in string interpolation (expression has type "ListContainer", placeholder has type "int | float | SupportsInt")  [str-format]
elftools/dwarf/descriptions.py:135: error: Incompatible types in string interpolation (expression has type "None", placeholder has type "int | float | SupportsInt")  [str-format]
elftools/dwarf/descriptions.py:149: error: Incompatible types in string interpolation (expression has type "int | None", placeholder has type "int | float | SupportsInt")  [str-format]
elftools/ehabi/ehabiinfo.py:58: error: Incompatible types in string interpolation (expression has type "int | None", placeholder has type "int | float | SupportsInt")  [str-format]
elftools/ehabi/ehabiinfo.py:164: error: Incompatible types in string interpolation (expression has type "int | None", placeholder has type "int")  [str-format]
elftools/ehabi/ehabiinfo.py:164: error: Incompatible types in string interpolation (expression has type "int | None", placeholder has type "int | float | SupportsInt")  [str-format]
elftools/ehabi/ehabiinfo.py:193: error: Incompatible types in string interpolation (expression has type "None", placeholder has type "int")  [str-format]
elftools/ehabi/ehabiinfo.py:204: error: Incompatible types in string interpolation (expression has type "int | None", placeholder has type "int")  [str-format]
elftools/elf/sections.py:106: error: Incompatible types in string interpolation (expression has type "str", placeholder has type "int")  [str-format]
elftools/elf/descriptions.py:349: error: Incompatible types in string interpolation (expression has type "str | int", placeholder has type "int")  [str-format]

Please have a 1st look.

Then we can decide on how to proceed, e.g. just merge it or try to extract a subset for only some public API files.

@pmhahn pmhahn mentioned this pull request Mar 19, 2025

@eliben eliben left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Just a couple of initial questions

In general, I'd really prefer someone with Python type checking experience to take a careful look at this

Comment thread elftools/common/utils.py
Comment thread elftools/common/utils.py

@eliben eliben left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

One question and general comment: would it be possible to split this PR to multiple? Even if just for the sake of review - could we start with a small-medium PR with a representative set of changes? It's OK if the intermediate steps don't fully type check until everything has landed

Comment thread elftools/common/construct_utils.py
@pmhahn

pmhahn commented Apr 1, 2025

Copy link
Copy Markdown
Contributor Author

One question and general comment: would it be possible to split this PR to multiple? Even if just for the sake of review - could we start with a small-medium PR with a representative set of changes? It's OK if the intermediate steps don't fully type check until everything has landed

Yes, I can do that. Any advise on how to split best? Internel / low-level / high-level?

@eliben

eliben commented Apr 1, 2025

Copy link
Copy Markdown
Owner

One question and general comment: would it be possible to split this PR to multiple? Even if just for the sake of review - could we start with a small-medium PR with a representative set of changes? It's OK if the intermediate steps don't fully type check until everything has landed

Yes, I can do that. Any advise on how to split best? Internel / low-level / high-level?

Yes, by layers could be a great way to slice it. Starting at the lowest possible and then progressing upwards

@eliben

eliben commented May 5, 2025

Copy link
Copy Markdown
Owner

Where do we stand with this PR? How much of it has already been added?

@pmhahn

pmhahn commented May 8, 2025

Copy link
Copy Markdown
Contributor Author

Where do we stand with this PR? How much of it has already been added?

Sorry for the delay, I'm currently busy otherwise. Hope to find some time next weekend.

@sevaa

sevaa commented Aug 28, 2025

Copy link
Copy Markdown
Collaborator

What's the status here?

@pmhahn

pmhahn commented Sep 8, 2025

Copy link
Copy Markdown
Contributor Author

What's the status here?

The bad news: Sadly I'm busy otherwise .
The good news: I found some time to update the PR and it again is in a state, where it could be merged:

pyright got unhappy, so I again had to re-introduce 6cb25b9 and fix some hints 7a0c243 , which already have been merged. mypy is still unhappy with a lot of things.

I have been experimenting with typeguard, which turns those type-hints into runtime checks. This allows validating those hints when running the test-suite. Sadly that drastically increases the runtime and – even more sad – shows several errors in my type annotations.
I have locally converted the unit-test to use pytest instead of Pythons built-in unittest as that allowed me to easily get coverage reports, but that also needs more work.

@Timmmm Timmmm left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This looks great to me. It would be good to split into smaller PRs - maybe you could do one with all the basic str, int, None types that are pretty obvious and then a second one with everything else?

On the other hand that's super tedious to do and nobody has time for that. If this were my project I think I would just merge it as-is and improve it in future PRs.

If this passes Pyright that's already a huge achievement and improvement.

Comment thread elftools/common/construct_utils.py
@sevaa

sevaa commented Nov 25, 2025

Copy link
Copy Markdown
Collaborator

I have a hunch this will never see the light of day. Would it be feasible to scale back the mission - type-annotate the user facing part of the API and mark the private stuff as off limits for the type checker?

@Timmmm

Timmmm commented Nov 25, 2025

Copy link
Copy Markdown

Yeah this should just be merged IMO. It's pretty much impossible to take an untyped Python project and add correct type hints to it all in one go. Once this is merged you can gradually fix the errors until it all type checks, and then enable type checking with Pyright in CI (or Pyrefly/Ty maybe by the time that actually happens!).

@k4lizen

k4lizen commented Jan 4, 2026

Copy link
Copy Markdown

Awesome work! I would love to see this merged, we got bitten by it recently here: pwndbg/pwndbg#3470 (comment)

@Rot127

Rot127 commented Mar 3, 2026

Copy link
Copy Markdown

+1 for just merging. Anything helps really.

@pmhahn

pmhahn commented Mar 9, 2026

Copy link
Copy Markdown
Contributor Author
  • I rebased my branch and fixed the conflicts.
  • mypy 1.19.1 is happy again
  • pyright 1.1.408 is also happy
  • pyrefly 0.62.0 is mostly happy except 3 constructs: Cannot assign to TYPE_CHECKING because it is imported as final facebook/pyrefly#3241
    ERROR Argument `Construct` is not assignable to parameter `object` with type `FormatField[int] | _InitialLengthAdapter` in function `list.append` [bad-argument-type]
       --> elftools/dwarf/callframe.py:410:23
    410 |         fields.append(formats[basic_encoding]('initial_location'))
        |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ERROR Argument `Construct` is not assignable to parameter `object` with type `FormatField[int] | _InitialLengthAdapter` in function `list.append` [bad-argument-type]
       --> elftools/dwarf/callframe.py:411:23
    411 |         fields.append(formats[basic_encoding]('address_range'))
        |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ERROR Cannot assign to `TYPE_CHECKING` because it is imported as final [bad-assignment]
      --> elftools/elf/structs.py:23:20
    23 | from .enums import *
       |                    ^
    
  • ty 0.0.32 still reports 43 issues. I have experienced the same in other projects that its reasoning is not more advanced as mypy and pyright — most often they`re worth investigating. I'm not going to work on this, but if anyone else has time: please feel free to have a look. PEP-767 might help in the future with some issues.
  • I have been running my branch with typeguards enabled, including the unit test:
    $ cat .venv/lib/python3.12/site-packages/sitecustomize.py 
    __import__("typeguard").install_import_hook("elftools")

Comment thread pyproject.toml
@eliben

eliben commented Mar 12, 2026

Copy link
Copy Markdown
Owner
  • I rebased my branch and fixed the conflicts.
  • mypy 1.19.1 is happy again
  • pyright 1.1.408 is also happy
  • ty 0.0.21 and pyrefly 0.55.0 both report many issues. I have experienced the same in other projects that their reasoning is not as advanced as mypy and pyright. I'm not going to work on this; if anyone else has time: please feel free to have a look.
  • I have been running my branch with typeguards enabled, including the unit test:
    $ cat .venv/lib/python3.12/site-packages/sitecustomize.py 
    __import__("typeguard").install_import_hook("elftools")
    

Thanks for all the work!
It's a shame about ty, because I've started mostly relying on it in my projects. I really dislike mypy due to its slowness and error messages.

I'll have to think about my strategy here overall...

@Timmmm

Timmmm commented Mar 12, 2026

Copy link
Copy Markdown

Ty and Pyrefly are not really ready for production yet. Pyright is definitely the way to go for now.

IMO Mypy is not worth thinking about. It is strictly worse than Pyright.

@pmhahn

pmhahn commented Mar 16, 2026

Copy link
Copy Markdown
Contributor Author

Ty and Pyrefly are not really ready for production yet. Pyright is definitely the way to go for now.

IMO Mypy is not worth thinking about. It is strictly worse than Pyright.

YMMV: They all have their pros and cons and currently find different things. I'm running fine with mypy for years.

I've spent some more time on this and have been able to reduce the number of issues:

  • add configuration for mypy, pyright, pyrefly, ty to only check elftools, but not
    • elftools.construct: vendored external project; not to be typed on request
    • examples: not yet types
    • tests: not yet typed
    • scripts: typed, but too many errors for now
  • Change elftools.construct.Construct.name into a read-only property; pyrefly Similar is needed for other "read-write-attributes", which ty check complains about. Strictly speaking its correct, but requires more code changes "just to silence type checkers". I'm unsure if that's okay or too much (for now) 🤔

@k4lizen

k4lizen commented Apr 24, 2026

Copy link
Copy Markdown

@eliben

I'll have to think about my strategy here overall...

Any updates on this? Merging this PR is not going to break any end users because there are no functional changes. The PR is +1,686 -1,088 which is not small but honestly not that big either, I've gone through it personally. pyelftools typing is currently broken for end users, this will only have a positive impact. If there are any issues, we will fix them when we get to them (I will gladly send a PR to fix such an issue if I encounter it), and again, they will not be functional, only part of peoples type checking pipeline. Most people have probably disabled type checking for pyelftools at this point (pwndbg has, for instance) since, again, it is currently flashing red due to the typed marker and there being no types.

By delaying merging this you are only creating more work for @pmhahn who has to rebase and resolve conflicts.

Could you elaborate what your thinking on this situation is?

@pmhahn

pmhahn commented Apr 25, 2026

Copy link
Copy Markdown
Contributor Author
  • I've again rebased my branch and resolved the merge conflict
  • After adding some minimal configuration for Meta's pyrefly and Astral's ty the number of issues was reduced drastically: most of them where for test/ and examples/, which Dropbox's mypy and Microsoft's pyright are also configured to ignore.
  • pyrefly has Cannot assign to TYPE_CHECKING because it is imported as final facebook/pyrefly#3241; I'd like to convert the from .enum import * to import .enum as e (or … as enum) and add that prefix to every usage if that is okay.
  • I want to look at the ty findings as they look valid in several cases. TBC…

@pmhahn

pmhahn commented May 26, 2026

Copy link
Copy Markdown
Contributor Author

Only the near final bits remain: This are now some special type hints:

  • Add Protocols as some type do not build a class hierarchy and thus requires a common base-class for typing: _RelocationFunction, _StringTable, _SymbolTable, _HashTable
  • Some more TypeVars to handle int vs. IntEnum and RelocationTables
  • Add type-annotations to loader(str): There is one unit-test which deliberately passed bytes, which needs (run-time) type-checking with typeguard to be disabled.
  • _reverse_dict() requires some extra work.
  • Some more @overloads for __getitem__() like functions to get more precise types in several locations for Section and Segment and describe_reg_name()
  • Some more assert to narrow types (DynamicSection, None)
  • Some special typing for the type-hierarchy Attribute and AttributeSection to handle its dynamic behavior, e.g. having a type for later creating.

The last (extra) bits as in #657

@pmhahn

pmhahn commented May 26, 2026

Copy link
Copy Markdown
Contributor Author

Only the near final bits remain: This are now some special type hints:

@eliben Have you missed this as you already closed #514 ?

@eliben

eliben commented May 26, 2026

Copy link
Copy Markdown
Owner

Only the near final bits remain: This are now some special type hints:

@eliben Have you missed this as you already closed #514 ?

I've closed it because it's not really useful and we've been coordinating the work here. For all practical purposes, type hints have already been added; we're now working through remaining details and enhancements.

Is this PR (611) now ready for a review and merge?

@pmhahn

pmhahn commented May 26, 2026

Copy link
Copy Markdown
Contributor Author

I've closed it because it's not really useful and we've been coordinating the work here. For all practical purposes, type hints have already been added; we're now working through remaining details and enhancements.

Okay.

Is this PR (611) now ready for a review and merge?

Yes, these are the final bits.

Comment thread elftools/elf/dynamic.py
Comment thread elftools/elf/hash.py
Comment thread elftools/elf/hash.py Outdated
Comment thread test/test_debuglink.py
from elftools.elf.elffile import ELFFile

try:
from typeguard import suppress_type_checks

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

What is this for?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

typeguard turns those type-annotations into runtime checks: I've been using this to validate, that my type-annotations actually match what is used at runtime. It found many errors in my initial typing and I've been running with it since.

As the commit message documents, 4e4ef81 changed the signature of the loader to use str only. But the test test_relative_loader_rejects_bytes_paths() calls ELFFile.make_relative_loader(bytes), which violates that type-hint: typeguard will detect this and raise an exception, which breaks the test. Thus typeguard must be disabled for that specific test (only):

$ python3 -m unittest discover . test_debuglink.py
..E.
======================================================================
ERROR: test_relative_loader_rejects_bytes_paths (test.test_debuglink.TestDebuglink.test_relative_loader_rejects_bytes_paths)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test/test_debuglink.py", line 124, in test_relative_loader_rejects_bytes_paths
    ELFFile.make_relative_loader(b'sample.elf')
  File "elftools/elf/elffile.py", line 117, in make_relative_loader
    def make_relative_loader(base_path: str) -> Callable[[str], IO[bytes]]:
  File ".venv/lib/python3.12/site-packages/typeguard/_functions.py", line 180, in check_argument_types_internal
    check_type_internal(value, annotation, memo)
  File ".venv/lib/python3.12/site-packages/typeguard/_checkers.py", line 994, in check_type_internal
    raise TypeCheckError(f"is not an instance of {qualified_name(origin_type)}")
typeguard.TypeCheckError: argument "base_path" (bytes) is not an instance of str

----------------------------------------------------------------------
Ran 4 tests in 0.216s

FAILED (errors=1)

If you drop this, no-body will be able to run the test-suite with typeguard unless you re-add it manually.

Alternative: drop the test or broaden the exception to also accept typeguard.TypeCheckError(Exception)

Comment thread test/test_hash.py

from elftools.elf.elffile import ELFFile
from elftools.elf.hash import ELFHashTable, GNUHashTable
from elftools.elf.hash import ELFHashTable, GNUHashTable, _SymbolTable

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I wonder: can we just leave type checking out of these tests?
It's a shame to convolute the test code for this; I'm not sure the value is worth it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Again typeguard: The tests pass None instead of some real-types, which typeguard does not like. As such unitest.mock.Mock must be used to at least fake enough to silence typeguard.
Being able to run the unit-test with typeguard enabled was a big win, so I prefer to keep this, but YMMV.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I don't get it.... I don't find any mentions of typeguard in the repo right now (other than pyproject.toml).

Generally, I'd like to avoid this extra dependency at all, if possible. Our goals may be different here -- my main interest is ensure that pyelftools's public API has type information so clients / users can benefit from it for their own code and documentation purposes. I care much less about the library's internals being deeply type checked, and tests even less so. So please let's simplify this as much as possible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

typeguard is no hard-dependency; that's why I've added that try: import typeguard; except ImportError: fallback thing so you can just use elftools as before.

It's similar to all those if TYPE_CHECKING: import …s which only add internal dependencies, when you run any type-checker. typeguard is just another one, but in contrast to mypy, pyright, pyrefly, ty being static – they only look at the code but do not execute it – typeguard is dynamic and checks types while executing the code. That's a great way to validate the type hints as there were many cases, where I started with – for example – bytes but than had it to change it to list[int] or add a | None, as reality was different from reading just the code and guessing things.

As soon as you extend pyelftools and add new functions/methods, you again will have the problem to add and validate new type hints; running the test suite with typeguard is a great way to get this almost for free – actually with (only) a drastic performance drop as all argument and return-values are validated, which takes time.

Now that ELFHashTable.__init__() is typed, your (or anybodies) editor will tell you that passing None to elffile (1st) and symboltable (4th argument) is invalid from a type-perspective. In this case its okay, but you don't want to add | None to those 2 argument just to have the tests clean as that would required checking for those Nones in may other locations.

While you could use elffile = symboltable = cast('Any', None) or add a # type: ignore there to silence the type-checkers, sadly that's not sufficient for typeguard as it explicitly checks, if the Protocol is implemented:

$ python3 -m unittest discover . -k test.test_hash.TestELFHash.test_empty_table_without_header
E
======================================================================
ERROR: test_empty_table_without_header (test.test_hash.TestELFHash.test_empty_table_without_header)
Verify we can handle an empty (0 byte) ELF hash section.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test/test_hash.py", line 67, in test_empty_table_without_header
    empty_hash_section = ELFHashTable(elffile, 0, 0, symboltable)
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "elftools/elf/hash.py", line 44, in __init__
    def __init__(
  File ".venv/lib/python3.12/site-packages/typeguard/_functions.py", line 180, in check_argument_types_internal
    check_type_internal(value, annotation, memo)
  File ".venv/lib/python3.12/site-packages/typeguard/_checkers.py", line 989, in check_type_internal
    checker(value, origin_type, args, memo)
  File ".venv/lib/python3.12/site-packages/typeguard/_checkers.py", line 862, in check_protocol
    raise TypeCheckError(
typeguard.TypeCheckError: argument "symboltable" (None) is not compatible with the _SymbolTable protocol because it has no method named 'get_symbol'

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (errors=1)

As unittest.mock.MagicMock is part of any standard Python since several years and it only needs to prototype to fake, importing _SymbolTable here to satisfy both static and dynamic type checkers here seems like a minor price to pay.

pmhahn added 8 commits May 27, 2026 07:40
key may either be an `int` or `EnumInt`. Declare a type requiring
minimum an `int`.

Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
Declare a protocol for the relocation functions, which all
implementations must follow. Callable is insufficient as some of those
functions are called with named arguments.

Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
Since 4e4ef81 ("Make make_relative_loader expect strings, not
bytes") `path` is of type `str`. Type it like that.

But there is one test, which explicitly passes `bytes` to test, if
the wrong type is rejected. This wrong type is detected by typeguard and
causes the test to fail. As such disable typeguard for that single test.

Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
Type helper function _reverse_dict() so that we get the correct type
automatically in every place it is used.

Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
Segment is similar to dict[Any, Any], which requires many explicit
type-casts in many locations.
To improve this, add some @Overloads to get better types similar to
TypedDict.

Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
Section is similar to dict[Any, Any], which requires many explicit
type-casts in many locations.
To improve this, add some @Overloads to get better types similar to
TypedDict.

Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
We know precisely, when describe_reg_name() will return a `str` and not
`None`. So save us from having to check for None in many locations, add
some @Overloads to get better types.

Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
Introduce TypedDict for RelocationTables to get better typing for
get_relocation_tables().

Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
pmhahn added 6 commits May 27, 2026 15:13
There are multiple classes, which can function as a StringTable. They do
not share a common super-class, which could be used for typing.

As such declare a Protocol, which captures the signature each of those
implementations must follow.

Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
There are multiple classes, which can function as a SymbolTable. They do
not share a common super-class, which could be used for typing.

As such declare a Protocol, which captures the signature each of those
implementations must follow.

Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
GNUHashTable and ELFHashTable to not share a common base class. As such
assigning one or the other to the same variable results in a type error.

Rename one instance.

Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
Assert that the fetched section is of type DynamicSection.

Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
For typing mirror the elf_assert() as assert.

Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
AttributesSubsubsection needs some type hints.

Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
@eliben
eliben merged commit e383a25 into eliben:main May 27, 2026
5 checks passed
@pmhahn
pmhahn deleted the typing branch May 28, 2026 06:15
@pmhahn

pmhahn commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

🥳

@k4lizen

k4lizen commented May 28, 2026

Copy link
Copy Markdown

yayy

thank you @pmhahn and @eliben :D

@k4lizen

k4lizen commented May 28, 2026

Copy link
Copy Markdown

could we get a new release cut with these changes?

@eliben

eliben commented May 28, 2026

Copy link
Copy Markdown
Owner

We can cut a new release if this effort is done. @pmhahn anything else remaining?

@pmhahn

pmhahn commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

We can cut a new release if this effort is done. @pmhahn anything else remaining?

@eliben No, all done.

@eliben

eliben commented May 29, 2026

Copy link
Copy Markdown
Owner

Please follow #660 for a new release, and use it to report any issues

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.

7 participants