Skip to content

Make --allow-redefinition mean --allow-redefinition-new#21276

Merged
JukkaL merged 13 commits intomasterfrom
allow-redef-rename
Apr 22, 2026
Merged

Make --allow-redefinition mean --allow-redefinition-new#21276
JukkaL merged 13 commits intomasterfrom
allow-redef-rename

Conversation

@JukkaL
Copy link
Copy Markdown
Collaborator

@JukkaL JukkaL commented Apr 20, 2026

Also now --allow-redefinition is the primary flag name for the previous --allow-redefinition-new flag, and the --allow-redefinition-new name is a (soft deprecated) alias.

The original --allow-redefinition behavior is still available via --allow-redefinition-old.

Also removed some now redundant --local-partial-types flags in tests.

@github-actions

This comment has been minimized.

@JukkaL
Copy link
Copy Markdown
Collaborator Author

JukkaL commented Apr 20, 2026

Looking at the mypy primer output, the results look overall like a net improvement, but supporting None partial types better would reduce the number of deviations significantly, i.e. this pattern:

# mypy: allow-redefinition

x = None

def f() -> None:
    global x
    x = 1  # Error if using --allow-redefinition, works otherwise

cc @ilevkivskyi in case you have ideas, as you worked on partial types recently

@ilevkivskyi
Copy link
Copy Markdown
Member

IIRC currently we simply disable None partial types when --allow-redefinition-new is set, and instead rely on widening. But widening doesn't work in this case either, since we prohibit widening variables from a different scope.

I don't know what was the motivation for disabling partial None types in the first place. If it was just because it felt redundant at the time (because local partial types didn't allow the above example anyway), then the simplest solution my be to just enable them back.

@ilevkivskyi
Copy link
Copy Markdown
Member

Actually after a quick experimentation the other option (allow widening of global names) looks simpler. I mean something like this:

         if name.kind == LDEF:
             # TODO: Consider reference to outer function as a different scope?
             return False
-        elif self.scope.top_level_function() is not None:
+        elif self.scope.top_level_function() is not None and name.kind != GDEF:
             # A non-local reference from within a function must refer to a different scope
             return True
         elif name.kind == GDEF and name.fullname.rpartition(".")[0] != self.tree.fullname:

in refers_to_different_scope(). IIUC this should be safe since such functions are considered part of module top-level anyway (i.e. have def_or_infer_vars = True). But I didn't look deeply into other possible consequences of this change.

Comment thread mypy/nativeparse.py Outdated
@@ -1,4 +1,4 @@
# mypy: allow-redefinition-new, local-partial-types
# mypy: allow-redefinition, local-partial-types
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These are actually both enabled in self-check, so this is a no-op.

@github-actions

This comment has been minimized.

@JukkaL
Copy link
Copy Markdown
Collaborator Author

JukkaL commented Apr 21, 2026

Actually after a quick experimentation the other option (allow widening of global names) looks simpler.

I have a variation of this mostly working. It's still a bit tricky, since we need to propagate the widened types to the globals binder, but it seems doable (if a little ugly).

JukkaL added a commit that referenced this pull request Apr 22, 2026
…21285)

Now this infers an optional type, for backward compat with
`--allow-redefinition-old`:

```py
# mypy: allow-redefinition-new
x = None

def foo() -> None:
    global x
    x = 1  # Ok
    
# Type of "x" is "int | None" here
```

There are a few non-trivial things. The implementation propagates the
widened type to the global scope, since otherwise `x` would have type
`None` at top level after the function. We also only allow widening if
the original type is `None`, to make reasoning about this easier.
Otherwise the type could be inferred from any number of functions. Also
this is sufficient to maintain backward compatibility.

I used coding agent assist with small incremental changes and careful
manual reviews.

See #21276 for additional context.
@github-actions
Copy link
Copy Markdown
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

spack (https://github.com/spack/spack)
+ lib/spack/spack/llnl/util/filesystem.py:1840: error: Argument 2 to "_log_file_access_issue" has incompatible type "str | Path"; expected "str"  [arg-type]
+ lib/spack/spack/llnl/util/filesystem.py:1844: error: Argument 1 to "appendleft" of "deque" has incompatible type "tuple[int, str | Path]"; expected "tuple[int, str]"  [arg-type]
+ lib/spack/spack/config.py:486: error: Unused "type: ignore" comment  [unused-ignore]
+ lib/spack/spack/util/web.py:83: error: Unused "type: ignore" comment  [unused-ignore]
+ lib/spack/spack/spec.py:4298: error: Item "VariantValue" of "Spec | Any | VariantValue" has no attribute "versions"  [union-attr]
+ lib/spack/spack/spec.py:4311: error: Item "VariantValue" of "Spec | Any | VariantValue" has no attribute "original_spec_format"  [union-attr]
+ lib/spack/spack/url_buildcache.py:1175: error: Need type annotation for "filename_to_mtime"  [var-annotated]
+ lib/spack/spack/solver/requirements.py:320: error: Argument 1 to "_parse_and_expand" of "RequirementParser" has incompatible type "Any | list[str]"; expected "str"  [arg-type]
+ lib/spack/spack/solver/requirements.py:336: error: Argument "message" to "RequirementRule" has incompatible type "Any | list[str] | None"; expected "str | None"  [arg-type]

steam.py (https://github.com/Gobot1234/steam.py)
- steam/id.py:91: error: Incompatible types in assignment (expression has type "int", variable has type "Type | None")  [assignment]
- steam/id.py:92: error: Incompatible types in assignment (expression has type "int", variable has type "Universe | None")  [assignment]
- steam/id.py:95: error: Incompatible types in assignment (expression has type "int", variable has type "Instance | None")  [assignment]
- steam/id.py:97: error: Unsupported operand types for <= ("int" and "None")  [operator]
- steam/id.py:97: note: Right operand is of type "Universe | None"
- steam/id.py:97: error: Unsupported operand types for > ("int" and "None")  [operator]
- steam/id.py:97: note: Left operand is of type "Universe | None"
- steam/id.py:99: error: Unsupported operand types for <= ("int" and "None")  [operator]
- steam/id.py:99: note: Right operand is of type "Type | None"
- steam/id.py:99: error: Unsupported operand types for > ("int" and "None")  [operator]
- steam/id.py:99: note: Left operand is of type "Type | None"
- steam/id.py:101: error: Unsupported operand types for <= ("int" and "None")  [operator]
- steam/id.py:101: note: Right operand is of type "Instance | None"
- steam/id.py:101: error: Unsupported operand types for > ("int" and "None")  [operator]
- steam/id.py:101: note: Left operand is of type "Instance | None"
- steam/id.py:111: error: Unsupported operand types for << ("None" and "int")  [operator]
- steam/id.py:111: note: Left operand is of type "Universe | None"
- steam/id.py:111: note: Left operand is of type "Type | None"
- steam/id.py:111: note: Left operand is of type "Instance | None"
- steam/id.py:482: error: Incompatible types in assignment (expression has type "int", variable has type "Instance")  [assignment]
- steam/id.py:486: error: Incompatible types in assignment (expression has type "int", variable has type "Instance")  [assignment]
- steam/id.py:488: error: Incompatible types in assignment (expression has type "int", variable has type "Instance")  [assignment]
- steam/id.py:490: error: Incompatible types in assignment (expression has type "int", variable has type "Instance")  [assignment]
- steam/id.py:492: error: Incompatible types in assignment (expression has type "int", variable has type "Instance")  [assignment]
+ steam/id.py:494: error: Argument "instance" to "ID" has incompatible type "int"; expected "Instance | None"  [arg-type]
- steam/id.py:558: error: Item "None" of "Any | None" has no attribute "get"  [union-attr]
- steam/protobufs/struct_messages.py:24: error: Incompatible types in assignment (expression has type "tuple[()]", variable has type "dict[str, Any]")  [assignment]
- steam/protobufs/msg.py:229: error: Incompatible types in assignment (expression has type "int", variable has type "EMsg")  [assignment]
+ steam/protobufs/msg.py:241: error: Argument 1 to "__init_subclass__" of "MessageMessageBase" has incompatible type "int"; expected "EMsg"  [arg-type]
- steam/app.py:388: error: Incompatible types in assignment (expression has type "OwnershipTicket", variable has type "AuthenticationTicket")  [assignment]
- steam/app.py:1090: error: Incompatible types in assignment (expression has type "PublishedFile[PartialUser]", variable has type "PublishedFileDetails")  [assignment]
- steam/abc.py:211: error: Incompatible types in assignment (expression has type "steam.comment.Comment[Self, PartialUser]", variable has type "steam.protobufs.community.GetCommentThreadResponse.Comment | None")  [assignment]
- steam/abc.py:668: error: Unsupported operand types for > ("datetime" and "None")  [operator]
- steam/abc.py:668: note: Left operand is of type "datetime | None"
- steam/abc.py:668: error: Unsupported operand types for < ("datetime" and "None")  [operator]
- steam/abc.py:668: note: Right operand is of type "datetime | None"
- steam/abc.py:796: error: Incompatible types in assignment (expression has type "PublishedFile[Self]", variable has type "PublishedFileDetails")  [assignment]
- steam/gateway.py:830: error: No overload variant of "wait_for" of "SteamWebSocket" matches argument types "int", "Callable[[UnifiedMsgT], bool] | Callable[[Any], Any]"  [call-overload]
+ steam/gateway.py:830: error: No overload variant of "wait_for" of "SteamWebSocket" matches argument types "int", "Callable[[UnifiedMsgT], bool]"  [call-overload]
- steam/state.py:464: error: Argument 2 to "pop" of "dict" has incompatible type "None"; expected "Clan"  [arg-type]
- steam/state.py:1038: error: Incompatible types in assignment (expression has type "Event[EventType, Clan]", variable has type "CMsgClientClanStateEvent")  [assignment]
- steam/state.py:1042: error: Incompatible types in assignment (expression has type "Announcement[Clan]", variable has type "CMsgClientClanStateEvent")  [assignment]
- steam/state.py:1208: error: Incompatible types in assignment (expression has type "PartialMember | None", variable has type "GroupMember | ClanMember")  [assignment]
- steam/state.py:1456: error: Argument 2 to "pop" of "dict" has incompatible type "None"; expected "ClanInvite | GroupInvite"  [arg-type]
- steam/state.py:1750: error: Incompatible types in assignment (expression has type "User", variable has type "int")  [assignment]
- steam/state.py:1753: error: Incompatible types in assignment (expression has type "int", variable has type "User")  [assignment]
- steam/state.py:1754: error: Incompatible types in assignment (expression has type "int", variable has type "User")  [assignment]
- steam/state.py:2372: error: Incompatible types in assignment (expression has type "PartialClan", variable has type "PartialUser")  [assignment]
- steam/state.py:2374: error: Incompatible types in assignment (expression has type "Event[EventType, PartialClan]", variable has type "PartialUser")  [assignment]
- steam/state.py:2376: error: Incompatible types in assignment (expression has type "Announcement[PartialClan]", variable has type "PartialUser")  [assignment]
- steam/state.py:2378: error: Incompatible types in assignment (expression has type "PublishedFile[Any | ClientUser | PartialUser] | None", variable has type "PartialUser")  [assignment]
- steam/state.py:2383: error: Incompatible types in assignment (expression has type "Review", variable has type "PartialUser")  [assignment]
- steam/state.py:2385: error: Incompatible types in assignment (expression has type "Post[PartialUser]", variable has type "PartialUser")  [assignment]
- steam/client.py:893: error: Incompatible types in assignment (expression has type "list[GameServer]", variable has type "list[IPsWithSteamIDsResponseServer]")  [assignment]
- steam/client.py:1285: error: Incompatible types in assignment (expression has type "float", variable has type "int")  [assignment]
- steam/ext/commands/commands.py:285: error: Incompatible types in assignment (expression has type "GroupMixin[Any | None] | Group[CogT, [VarArg(Any), KwArg(Any)], Any] | None", variable has type "Self")  [assignment]
+ steam/ext/commands/commands.py:831: error: Incompatible return value type (got "Any | Command[Any, Any, Any]", expected "C")  [return-value]

pwndbg (https://github.com/pwndbg/pwndbg)
+ pwndbg/dbg_mod/gdb/__init__.py: note: In member "cast" of class "GDBValue":
+ pwndbg/dbg_mod/gdb/__init__.py:1556: error: Name "type" already defined on line 1554  [no-redef]
+ pwndbg/dbg_mod/gdb/__init__.py: note: At top level:
+ pwndbg/dintegration/__init__.py: note: In member "connect" of class "IntegrationManager":
+ pwndbg/dintegration/__init__.py:509: error: Argument 1 to "DecompilerConnection" has incompatible type "_Method"; expected "ServerProxy"  [arg-type]
+ pwndbg/aglib/godbg.py:851: error: Argument 1 to "fmt_bytes" of "FormatOpts" has incompatible type "bytes | bytearray"; expected "bytes"  [arg-type]
+ pwndbg/commands/procinfo.py: note: In member "status" of class "Process":
+ pwndbg/commands/procinfo.py:183: error: Argument 1 to "append" of "list" has incompatible type "str"; expected "int"  [arg-type]
+ pwndbg/commands/kmod.py: note: In function "kmod":
+ pwndbg/commands/kmod.py:95: error: Argument 2 to "add_symbol_file" of "Process" has incompatible type "object"; expected "int | None"  [arg-type]
+ pwndbg/commands/ptmalloc2.py:1589: error: Unused "type: ignore" comment  [unused-ignore]
+ pwndbg/dbg_mod/lldb/__init__.py: note: In member "cast" of class "LLDBValue":
+ pwndbg/dbg_mod/lldb/__init__.py:784: error: Name "type" already defined on line 782  [no-redef]
+ pwndbg/dbg_mod/lldb/__init__.py: note: In member "vmmap" of class "LLDBProcess":
+ pwndbg/dbg_mod/lldb/__init__.py:1153: error: Name "pages" already defined on line 1143  [no-redef]
+ pwndbg/dbg_mod/lldb/__init__.py: note: In member "create_value" of class "LLDBProcess":
+ pwndbg/dbg_mod/lldb/__init__.py:1480: error: Name "u64" already defined on line 1476  [no-redef]
+ pwndbg/dbg_mod/lldb/__init__.py: note: In member "arch" of class "LLDBProcess":
+ pwndbg/dbg_mod/lldb/__init__.py:1866: error: Argument "name" to "ArchDefinition" has incompatible type "Any | str"; expected "Literal['x86-64', 'i386', 'i8086', 'mips', 'aarch64', 'arm', 'armcm', 'rv32', 'rv64', 'sparc', 'powerpc', 'loongarch64', 's390x', 'hexagon']"  [arg-type]
+ pwndbg/dbg_mod/lldb/repl/__init__.py: note: In function "parse":
+ pwndbg/dbg_mod/lldb/repl/__init__.py:819: error: Need type annotation for "raw"  [var-annotated]
+ pwndbg/dbg_mod/lldb/repl/__init__.py: note: At top level:

egglog-python (https://github.com/egraphs-good/egglog-python)
+ python/egglog/ipython_magic.py:41: error: Item "EGraph" of "Any | EGraph" has no attribute "to_graphviz_string"  [union-attr]
+ python/egglog/egraph.py:471: error: Unused "type: ignore" comment  [unused-ignore]
+ python/egglog/egraph.py:471: error: Item "function" of "Callable[..., Any] | Any" has no attribute "__wrapped__"  [union-attr]
+ python/egglog/egraph.py:471: note: Error code "union-attr" not covered by "type: ignore[attr-defined]" comment

Tanjun (https://github.com/FasterSpeeding/Tanjun)
+ tanjun/permissions.py:231: error: Incompatible types in assignment (expression has type "Mapping[Snowflake, Role] | Cache | None", variable has type "Mapping[Snowflake, Role] | None")  [assignment]
- tanjun/dependencies/limiters.py:812: error: Incompatible types in assignment (expression has type "_Cooldown", variable has type "AbstractCooldownBucket | None")  [assignment]
- tanjun/dependencies/limiters.py:814: error: Item "AbstractCooldownBucket" of "AbstractCooldownBucket | None" has no attribute "check"  [union-attr]
- tanjun/dependencies/limiters.py:814: error: Item "None" of "AbstractCooldownBucket | None" has no attribute "check"  [union-attr]
- tanjun/dependencies/limiters.py:850: error: Incompatible types in assignment (expression has type "_Cooldown | None", variable has type "AbstractCooldownBucket | None")  [assignment]
- tanjun/commands/slash.py:3222: error: Incompatible types in assignment (expression has type "def (AutocompleteContext, float, /, *Any, **Any) -> Coroutine[Any, Any, None] | None", variable has type "def (AutocompleteContext, str, /, *Any, **Any) -> Coroutine[Any, Any, None] | None")  [assignment]
- tanjun/commands/slash.py:3225: error: Incompatible types in assignment (expression has type "def (AutocompleteContext, int, /, *Any, **Any) -> Coroutine[Any, Any, None] | None", variable has type "def (AutocompleteContext, str, /, *Any, **Any) -> Coroutine[Any, Any, None] | None")  [assignment]
- tanjun/commands/slash.py:3235: error: Argument 1 to "call_with_async_di" of "Context" has incompatible type "def (AutocompleteContext, str, /, *Any, **Any) -> Coroutine[Any, Any, None]"; expected "Callable[..., Coroutine[Any, Any, Never] | Never]"  [arg-type]
+ tanjun/commands/slash.py:3235: error: Argument 1 to "call_with_async_di" of "Context" has incompatible type "def (AutocompleteContext, str, /, *Any, **Any) -> Coroutine[Any, Any, None] | def (AutocompleteContext, float, /, *Any, **Any) -> Coroutine[Any, Any, None] | def (AutocompleteContext, int, /, *Any, **Any) -> Coroutine[Any, Any, None]"; expected "Callable[..., Coroutine[Any, Any, Never] | Never]"  [arg-type]

pandera (https://github.com/pandera-dev/pandera)
- pandera/engines/utils.py:73: error: Unused "type: ignore[call-overload]" comment  [unused-ignore]
+ pandera/engines/utils.py:73: error: Unused "type: ignore[union-attr, call-overload]" comment  [unused-ignore]
+ pandera/engines/pandas_engine.py:268: error: Name "pandera_dtype" already defined on line 265  [no-redef]
+ pandera/engines/pandas_engine.py:1457: error: Unused "type: ignore" comment  [unused-ignore]
- pandera/typing/pandas.py:211: error: Unused "type: ignore" comment  [unused-ignore]
+ pandera/typing/pandas.py:238: error: Unused "type: ignore" comment  [unused-ignore]
- pandera/typing/pandas.py:249: error: Unused "type: ignore" comment  [unused-ignore]
+ pandera/io/xarray_io.py:248: error: Need type annotation for "data_vars"  [var-annotated]
+ pandera/backends/pandas/container.py:774: error: Unused "type: ignore" comment  [unused-ignore]
+ pandera/backends/pandas/container.py:776: error: Incompatible return value type (got "T | Any | None", expected "T")  [return-value]
- pandera/backends/pandas/array.py:454: error: Incompatible types in assignment (expression has type "str", variable has type "Series[Any] | DataFrame | None")  [assignment]
- pandera/backends/pandas/array.py:456: error: Incompatible types in assignment (expression has type "str", variable has type "Series[Any] | DataFrame | None")  [assignment]
- pandera/api/pandas/array.py:251: error: Redundant cast to "Series[Any]"  [redundant-cast]
+ pandera/io/pandas_io.py:994: error: Incompatible types in assignment (expression has type "dict[str, str]", target has type "str")  [assignment]
- pandera/backends/pandas/components.py:980: error: Incompatible types in assignment (expression has type "Hashable", variable has type "str")  [assignment]
+ pandera/decorators.py:397: error: Invalid index type "str | int" for "list[Any]"; expected type "SupportsIndex"  [index]
+ pandera/decorators.py:401: error: No return value expected  [return-value]
+ pandera/api/pandas/model.py:196: error: Unused "type: ignore" comment  [unused-ignore]
+ tests/pandas/test_schema_statistics.py:46: error: Unused "type: ignore" comment  [unused-ignore]
+ tests/pandas/test_schema_inference.py:26: error: Unused "type: ignore" comment  [unused-ignore]
+ pandera/strategies/xarray_strategies.py:297: error: Need type annotation for "coord_specs"  [var-annotated]
+ pandera/strategies/xarray_strategies.py:359: error: Need type annotation for "coord_specs"  [var-annotated]
+ pandera/api/polars/model.py:82: error: Unused "type: ignore" comment  [unused-ignore]
+ pandera/api/ibis/model.py:86: error: Unused "type: ignore" comment  [unused-ignore]

@JukkaL
Copy link
Copy Markdown
Collaborator Author

JukkaL commented Apr 22, 2026

The primer looks as expected -- this is mostly an improvement, but a few cases are regressed, notably things like this:

def f() -> None:
    x: int = 1
    print(x)
    x: str = "a"  # Ok with --allow-redefinition-old but not new
    print(x)

We could support the above use case by using a variable renaming pass for re-annotated variables, but I'm not sure if this is worth it, as the impact seems fairly small.

Also this was false negative (None type wasn't inferred for x) which is fixed when using --allow-redefinition now, but requires a type annotation:

def f() -> None:
    x = None  # Now needs type annotation
    if int():
        x = {}
        x["a"] = 1
    reveal_type(x)  # Now correctly inferred as an optional type

@ilevkivskyi
Copy link
Copy Markdown
Member

We could support the above use case by using a variable renaming pass for re-annotated variables, but I'm not sure if this is worth it, as the impact seems fairly small.

Yeah, I was thinking about that, and IMO although some users may find that useful it will definitely complicate things. We would need to decide what kind of behavior we want in various edge cases, for example it is not clear whether we need to allow things like this:

if foo():
    x: int = 1
else:
    x: str = "a"

and other variations. Also if we will decide to support this at some point, then it will not be a breaking change, so we don't really need to decide now (taking into account that impact for existing users is not too big).

Copy link
Copy Markdown
Member

@ilevkivskyi ilevkivskyi left a comment

Choose a reason for hiding this comment

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

LG, thanks! We will need to emphasize in the CHANGELOG/release notes that only non-annotated variables can be redefined.

We can potentially use the "name" vs "variable" terminology to refer to this in the docs.

@JukkaL JukkaL merged commit 4c68d1b into master Apr 22, 2026
25 checks passed
@JukkaL JukkaL deleted the allow-redef-rename branch April 22, 2026 18:02
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