Title: Analysis of some mypy adoption problems and the introduction of type: ignore comments · Issue #652 · canopen-python/canopen · GitHub
Open Graph Title: Analysis of some mypy adoption problems and the introduction of type: ignore comments · Issue #652 · canopen-python/canopen
X Title: Analysis of some mypy adoption problems and the introduction of type: ignore comments · Issue #652 · canopen-python/canopen
Description: Analysis of type: ignore Comments and Potential Fixes This issue explains why multiple type: ignore comments were introduced during the mypy adoption (PRs #647, #649, #650, #651) and what API changes or refactorings would be needed to el...
Open Graph Description: Analysis of type: ignore Comments and Potential Fixes This issue explains why multiple type: ignore comments were introduced during the mypy adoption (PRs #647, #649, #650, #651) and what API chang...
X Description: Analysis of type: ignore Comments and Potential Fixes This issue explains why multiple type: ignore comments were introduced during the mypy adoption (PRs #647, #649, #650, #651) and what API chang...
Opengraph URL: https://github.com/canopen-python/canopen/issues/652
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"Analysis of some mypy adoption problems and the introduction of type: ignore comments","articleBody":"# Analysis of `type: ignore` Comments and Potential Fixes\n\nThis issue explains why multiple `type: ignore` comments were introduced\nduring the mypy adoption (PRs #647, #649, #650, #651) and what API changes\nor refactorings would be needed to eliminate them.\n\n## Root Cause: Dynamically Typed Value Pipeline\n\nThe core issue is that `ODVariable.decode_raw()` returns\n`Union[int, float, str, bytes, bytearray]` and `encode_raw()` accepts the same\nunion. This \"wide\" union type propagates through the entire `Variable` property\nchain (`raw`, `phys`, `desc`, `bits`), making it impossible for mypy to know\nwhich concrete type is in play at any given point.\n\nIn practice, the concrete type is determined by `ODVariable.data_type` at\nruntime — but mypy cannot track this correlation.\n\n---\n\n## Per-File Analysis\n\n### `canopen/objectdictionary/__init__.py` (7 ignores)\n\n#### 1. `encode_raw()` — string operations on Union type\n```python\nreturn value.encode(\"ascii\") # type: ignore[union-attr]\nreturn value.encode(\"utf_16_le\") # type: ignore[union-attr]\nreturn bytes(value) # type: ignore[arg-type]\n```\n**Why:** After the `isinstance(value, (bytes, bytearray))` guard, `value` is\nnarrowed to `Union[int, float, str]`. The `VISIBLE_STRING` / `UNICODE_STRING`\nbranches assume `value` is `str`, but mypy doesn't correlate `data_type` with\nthe value type.\n\n**Fix:** Use `@overload` to create typed variants of `encode_raw()`:\n```python\n@overload\ndef encode_raw(self, value: str) -\u003e bytes: ...\n@overload\ndef encode_raw(self, value: int) -\u003e bytes: ...\n@overload\ndef encode_raw(self, value: bytes) -\u003e bytes: ...\n```\nOr split into `encode_string()`, `encode_number()`, `encode_bytes()`.\nThis is a significant API change.\n\n#### 2. `encode_raw()` — min/max comparisons\n```python\nif self.min is not None and value \u003c self.min: # type: ignore[operator]\nif self.max is not None and value \u003e self.max: # type: ignore[operator]\n```\n**Why:** At this point `value` is `Union[int, float, str]`, but the comparison\nonly makes sense for numbers. The `str` type doesn't support `\u003c`/`\u003e` with `int`.\n\n**Fix:** Add an explicit `isinstance(value, (int, float))` guard before the\ncomparisons. This would be safe and backward-compatible:\n```python\nif isinstance(value, (int, float)):\n if self.min is not None and value \u003c self.min:\n ...\n```\n\n#### 3. `encode_phys()` — division and return\n```python\nvalue = int(round(value / self.factor)) # type: ignore[operator]\nreturn value # type: ignore[return-value]\n```\n**Why:** `value` is `Union[int, bool, float, str, bytes]` but division only\nworks on numbers. The `return value` may still be `str` or `bytes` when\n`data_type` is not in `INTEGER_TYPES`.\n\n**Fix:** Change `encode_phys()` to accept and return `Union[int, float]` only.\nCallers that pass `str`/`bytes` would need to go through `encode_raw()` instead.\nThis is an API change.\n\n#### 4. File handling in `export_od()` / `import_od()`\n```python\ndest.close() # type: ignore[union-attr]\nfilename = source.name # type: ignore[union-attr]\n```\n**Why:** `dest` is `Union[str, TextIO, None]` — after the `opened_here` guard,\nit's always `TextIO`. `source` after `hasattr(source, \"read\")` is `TextIO`.\n\n**Fix:** Introduce a local variable with explicit narrowing:\n```python\nif opened_here:\n assert isinstance(dest, TextIO)\n dest.close()\n```\nOr restructure using a context manager pattern. This is a safe, non-breaking\nchange but requires some refactoring of the function flow.\n\n---\n\n### `canopen/variable.py` (6 ignores)\n\n#### 5. `raw` property flows into type-specific methods\n```python\nvalue = self.od.decode_phys(self.raw) # type: ignore[arg-type]\nvalue = self.od.decode_desc(self.raw) # type: ignore[arg-type]\nself.raw = self.od.encode_desc(desc) # type: ignore[assignment]\n```\n**Why:** `self.raw` returns `Union[int, bool, float, str, bytes]`, but\n`decode_phys()` expects `int` and `decode_desc()` expects `int`.\n\n**Fix:** This is a consequence of the root cause. Possible approaches:\n1. Make `decode_phys()` / `decode_desc()` accept the full Union and do\n runtime validation internally.\n2. Introduce a `Variable.raw_int` property that returns `int` and use it\n in `phys` and `desc` properties.\n3. Use `Generic` types parameterized by `data_type` — very complex.\n\n#### 6. `Bits` class raw type\n```python\nreturn self.variable.od.decode_bits(self.raw, ...) # type: ignore[arg-type]\nself.raw = self.variable.od.encode_bits(self.raw, ...) # type: ignore[arg-type]\n```\n**Why:** `Bits.raw` is `Union[int, bool, float, str, bytes]` (copied from\n`Variable.raw`), but `decode_bits()` / `encode_bits()` expect `int`.\n\n**Fix:** `Bits` should store `raw` as `int` and cast on read/write:\n```python\ndef read(self):\n raw = self.variable.raw\n assert isinstance(raw, int)\n self.raw: int = raw\n```\n\n#### 7. `write()` method — desc assignment\n```python\nself.desc = value # type: ignore[assignment]\n```\n**Why:** `value` is `Union[int, bool, float, str, bytes]` but `desc` setter\nexpects `str`.\n\n**Fix:** Add type narrowing or runtime validation in `write()`:\n```python\nelif fmt == \"desc\":\n if not isinstance(value, str):\n raise TypeError(\"desc format requires str value\")\n self.desc = value\n```\n\n---\n\n### `canopen/network.py` (2 ignores)\n\n#### 8. Node construction with Optional OD\n```python\nnode = RemoteNode(node, object_dictionary) # type: ignore[arg-type]\nnode = LocalNode(node, object_dictionary) # type: ignore[arg-type]\n```\n**Why:** `object_dictionary` is `Union[str, ObjectDictionary, None]`, but\nnode constructors don't accept `None`.\n\n**Fix:** Make `RemoteNode` / `LocalNode` constructors accept `None` for\n`object_dictionary` and create an empty `ObjectDictionary()` internally.\nOr add proper validation in `add_node()` / `create_node()`:\n```python\nif object_dictionary is None:\n raise ValueError(\"object_dictionary is required when node is an int\")\n```\nNote: The current code intentionally allows `None` to flow through — e.g.\nwhen `upload_eds` fails, the node is still created with `None` OD. This is\na design decision that would need discussion.\n\n---\n\n### `canopen/sdo/base.py` (1 ignore)\n\n#### 9. `SdoArray.__len__()` return type\n```python\nreturn self[0].raw # type: ignore[return-value]\n```\n**Why:** `self[0].raw` is `Union[int, bool, float, str, bytes]` but\n`__len__` must return `int`.\n\n**Fix:** Cast explicitly:\n```python\ndef __len__(self) -\u003e int:\n value = self[0].raw\n assert isinstance(value, int)\n return value\n```\n\n---\n\n## Summary of Required Changes\n\n| Priority | Change | Impact | Breaking? |\n|----------|--------|--------|-----------|\n| Low | `encode_raw()` min/max guards | 2 ignores removed | No |\n| Low | `Bits.raw` as `int` | 2 ignores removed | No |\n| Low | `write()` desc validation | 1 ignore removed | No |\n| Low | `SdoArray.__len__` cast | 1 ignore removed | No |\n| Low | File handling narrowing | 2 ignores removed | No |\n| Medium | `decode_phys`/`decode_desc` accept Union | 3 ignores removed | Minor |\n| Medium | Node constructor accept None | 2 ignores removed | Minor |\n| High | `encode_raw` overloads or split | 3 ignores removed | Yes |\n| High | `encode_phys` type narrowing | 2 ignores removed | Yes |\n\nThe **low priority** changes (10 ignores) can be done without any API changes.\nThe **medium/high priority** changes (8 ignores) require API discussion and\npotentially affect downstream users.\n","author":{"url":"https://github.com/bizfsc","@type":"Person","name":"bizfsc"},"datePublished":"2026-04-29T19:25:22.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":0},"url":"https://github.com/652/canopen/issues/652"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:3aba9783-a74c-b98f-634e-35f9fffa3f52 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | DE0E:3081D4:4B813B6:63B4EAF:6A5D9BCC |
| html-safe-nonce | 69fc017faeb37f7180697aad53fe7b30182d6cd9a2384847a30ee4238f038c46 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJERTBFOjMwODFENDo0QjgxM0I2OjYzQjRFQUY6NkE1RDlCQ0MiLCJ2aXNpdG9yX2lkIjoiMzQ5NzQxODg1OTM3MjYxNTE2IiwicmVnaW9uX2VkZ2UiOiJpYWQiLCJyZWdpb25fcmVuZGVyIjoiaWFkIn0= |
| visitor-hmac | 4cbfc028e519442cd220726f88206d9e488e25f7c544a4383ac4eba2a7b41003 |
| hovercard-subject-tag | issue:4353131946 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/canopen-python/canopen/652/issue_layout |
| twitter:image | https://opengraph.githubassets.com/5d57b7b2e6b8dc249001b9b0cc327b9f921f601ff84097e4407d6427883a6f42/canopen-python/canopen/issues/652 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/5d57b7b2e6b8dc249001b9b0cc327b9f921f601ff84097e4407d6427883a6f42/canopen-python/canopen/issues/652 |
| og:image:alt | Analysis of type: ignore Comments and Potential Fixes This issue explains why multiple type: ignore comments were introduced during the mypy adoption (PRs #647, #649, #650, #651) and what API chang... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | bizfsc |
| hostname | github.com |
| expected-hostname | github.com |
| None | 5290d7e14309ad1e76106a9c4237bd1041517e83ea182c8ab756752cb0c6940b |
| turbo-cache-control | no-preview |
| go-import | github.com/canopen-python/canopen git https://github.com/canopen-python/canopen.git |
| octolytics-dimension-user_id | 200581454 |
| octolytics-dimension-user_login | canopen-python |
| octolytics-dimension-repository_id | 68737600 |
| octolytics-dimension-repository_nwo | canopen-python/canopen |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 68737600 |
| octolytics-dimension-repository_network_root_nwo | canopen-python/canopen |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 9c975978430e9ad293956f2bbdaf153b1bd84a99 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width