Skip to content

Add type hints#611

Open
pmhahn wants to merge 38 commits intoeliben:mainfrom
pmhahn:typing
Open

Add type hints#611
pmhahn wants to merge 38 commits intoeliben:mainfrom
pmhahn:typing

Conversation

@pmhahn
Copy link
Contributor

@pmhahn pmhahn commented Mar 19, 2025

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
Copy link
Owner

@eliben eliben left a comment

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

Copy link
Owner

@eliben eliben left a comment

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

@pmhahn
Copy link
Contributor Author

pmhahn commented Apr 1, 2025

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
Copy link
Owner

eliben commented Apr 1, 2025

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
Copy link
Owner

eliben commented May 5, 2025

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

@pmhahn
Copy link
Contributor Author

pmhahn commented May 8, 2025

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
Copy link
Contributor

sevaa commented Aug 28, 2025

What's the status here?

@pmhahn
Copy link
Contributor Author

pmhahn commented Sep 8, 2025

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.

Copy link

@Timmmm Timmmm left a comment

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.

@sevaa
Copy link
Contributor

sevaa commented Nov 25, 2025

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
Copy link

Timmmm commented Nov 25, 2025

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
Copy link

k4lizen commented Jan 4, 2026

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

@Rot127
Copy link

Rot127 commented Mar 3, 2026

+1 for just merging. Anything helps really.

pmhahn added 10 commits March 9, 2026 06:38
Convert from legacy `collections.namedtuple` to `typing.NamedTuple` to
allow adding type hints.

Signed-off-by: Philipp Hahn <[email protected]>
`pyelftools` does not use class hierarchies in all cases, but relies on
_duck typing_.
Introduce Protocols for those cases to allow type hinting.

Signed-off-by: Philipp Hahn <[email protected]>
- [x] `pyright`
- [x] `mypy` with several issues:
- [ ] `pyrefly`
- [ ] `ty`
- [ ] `typeguard`
  To run `pyelftools` under `typeguards` create the file
  `.venv/lib/python3.12/site-packages/sitecustomize.py` with line
  `__import__("typeguard").install_import_hook("elftools")`
> Cannot determine type of "dwarfinfo"
> Cannot determine type of "structs"

Signed-off-by: Philipp Hahn <[email protected]>
Several classes inheriting from `Construct`, which only has a generic
`__getattr__(self, name: str) -> Any`.
To improve typing explicitly name several attributes with their type.

For `name` we know that it (almost) never will be `None` – an unnamed
entity does not make much sense. Overwrite the type-hint `str | None`
inherited from `Construct` with just `str` which saves us from a ton if
`name is not None` checks.

Signed-off-by: Philipp Hahn <[email protected]>
Several variable may have a `Union` type.
Explicitly assert that those variables have the expected type.

Signed-off-by: Philipp Hahn <[email protected]>
Several variable may have an `Optional` type and can be `None`.
Explicitly assert that those variables are `not None`.

Signed-off-by: Philipp Hahn <[email protected]>
Declare `lsda_pointer` and `aug_dict` and `last_line_in_CIE` before the
conditional. Static type checking will complain otherwise that variables
might not be declared on the `else` case.

Signed-off-by: Philipp Hahn <[email protected]>
`Dwarf_dw_form` contains `None` values, but type checkers fail to do a
static lookup to see, that they are constant and `not None`.

Signed-off-by: Philipp Hahn <[email protected]>
pmhahn added 23 commits March 9, 2026 08:50
`ENUM_D_TAG` is a constant, but built by code.
Convert it into a dict-comprehension.

Signed-off-by: Philipp Hahn <[email protected]>
`get_symbol()` may return `None` and `symbol.name` would raise an
`AttributeError`. Check for `not None` explicitly.

Signed-off-by: Philipp Hahn <[email protected]>
`entry_translate` may contain `None`.
Silence type checking.

Signed-off-by: Philipp Hahn <[email protected]>
An unknown `ST_SHNDC` `str` will raise `ValueError`.

Signed-off-by: Philipp Hahn <[email protected]>
`get_table_offset()` may return `tuple[…, None]`.

Signed-off-by: Philipp Hahn <[email protected]>
`entry_translate` may contain `None` in the DWARF-5-case.
Silence type checking.

Signed-off-by: Philipp Hahn <[email protected]>
Code explicitly checks for `AttributeError`, but `mypy` is picky here.

Signed-off-by: Philipp Hahn <[email protected]>
`params` is first declared as a `tuple` and then changed to `str`, which
static type checkers like `mypy` do not like.

Rename the first variable.

Signed-off-by: Philipp Hahn <[email protected]>
`reveal_type` is first declared as `TypeDesc` and then changed to `str`,
which static type checkers like `mypy` do not like.

Rename the first variable.

PS: Better rename `reveal_type()` to somethings else as there is
`typing.reveal_type()`[^1].

[^1]: https://docs.python.org/3/library/typing.html#typing.reveal_type

Signed-off-by: Philipp Hahn <[email protected]>
`all_offsets` is first declared as a `set` and then changed to `list`,
which static type checkers like `mypy` do not like.

Rename the first variable.

Signed-off-by: Philipp Hahn <[email protected]>
`get_parent()` may return `None`, in wich case `parent.tag` would raise
an `AttributeError`. Make the check explicit for type checking.

Signed-off-by: Philipp Hahn <[email protected]>
Raise `ValueError` in all `else` cases.

Signed-off-by: Philipp Hahn <[email protected]>
Combine the `if` statements to help type checking: Otherwise the 2nd
DWARF-5-case need another case to check for `die not None`.

FYI: This is a behavioral change as the file position gets changed in
the error case.

Signed-off-by: Philipp Hahn <[email protected]>
`dict.get() -> … | None`

Signed-off-by: Philipp Hahn <[email protected]>
Also store the reference to `RelocationTable` in a local variable to
help type checkers using the correct type when checking `entry_size`.

Signed-off-by: Philipp Hahn <[email protected]>
`Dynamic` expects an instance following the protocol `_StringTable`, but
`get_section()` just returns a `Section`.

Signed-off-by: Philipp Hahn <[email protected]>
Explicitly return `None` to silence `mypy`.

Signed-off-by: Philipp Hahn <[email protected]>
Also store the reference to `Container` in a local variable to help type
checkers using the correct type when checking `bloom_size` and
`nbuckets`.

Signed-off-by: Philipp Hahn <[email protected]>
`Attribute` and its related classes `Attribute(Sub)*Section` have two
concrete sub-classes for ARM and RISCV with *different* constructors.
This confuses type checkers like `mypy` and `pyright` and humans like
me, as this is not type safe: the `AttributeSubsubsection` classes
are factory methods for `Attributes`, but their actual signature
differs from the prototype. Basically you have to tell type-checkers:
Expect a class with this signature, but you will get back an instance of
another class having a different signature for `__init__()`.

After having tried `Protocols` and `TypeVars` I gave up and
re-implemented all 3*4 classes to have sane constructors:
- The concrete sub-classes for ARM and RISCV sections only set different
  class variables; no extra code needed.
- For `…Attributes` there's a new class method as `structs` must be
  handled at runtime.

Signed-off-by: Philipp Hahn <[email protected]>
Printed value may not be an integer, which '%x' expects.
Use '%r' to print the `repr()`esentation.

Signed-off-by: Philipp Hahn <[email protected]>
`_num_entry` is types as `int|None`, but calling `num_entry()` will make
it `int`. Type checkers don't see this and will complain about
`_num_entry` being `None`, which is incompatible with type format `%d`.

Signed-off-by: Philipp Hahn <[email protected]>
To run the unittest with typeguard mock ELFFile and _SymbolTable instead
of passing in `None`.

Signed-off-by: Philipp Hahn <[email protected]>
@pmhahn
Copy link
Contributor Author

pmhahn commented Mar 9, 2026

  • 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")

@eliben
Copy link
Owner

eliben commented Mar 12, 2026

  • 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
Copy link

Timmmm commented Mar 12, 2026

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
Copy link
Contributor Author

pmhahn commented Mar 16, 2026

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) 🤔

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.

6 participants