Skip to content

[ty] Emit a diagnostic on incorrect applications of the legacy convention for specifying positional-only parameters#22943

Merged
AlexWaygood merged 5 commits intomainfrom
alex/pre-pep570-lint
Jan 31, 2026
Merged

[ty] Emit a diagnostic on incorrect applications of the legacy convention for specifying positional-only parameters#22943
AlexWaygood merged 5 commits intomainfrom
alex/pre-pep570-lint

Conversation

@AlexWaygood
Copy link
Member

@AlexWaygood AlexWaygood commented Jan 29, 2026

Summary

The typing spec states:

Consistent with PEP 570 syntax, positional-only parameters cannot appear after parameters that accept keyword arguments. Type checkers should enforce this requirement:

def g(x: int, __y: int) -> None: ...  # Type error

This PR adds that lint.

Test Plan

mdtests and snapshots

@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Jan 29, 2026
Comment on lines 16040 to 16072
impl<'a, K, V> IntoIterator for &'a VecMap<K, V>
where
K: Eq,
K: std::fmt::Debug,
V: std::fmt::Debug,
{
type Item = (&'a K, &'a V);
type IntoIter = VecMapIterator<'a, K, V>;

fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}

struct VecMapIterator<'a, K, V> {
inner: std::slice::Iter<'a, (K, V)>,
}

impl<'a, K, V> Iterator for VecMapIterator<'a, K, V> {
type Item = (&'a K, &'a V);

fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|(k, v)| (k, v))
}
}

impl<K, V> std::iter::FusedIterator for VecMapIterator<'_, K, V> {}

impl<K, V> ExactSizeIterator for VecMapIterator<'_, K, V> {
fn len(&self) -> usize {
self.inner.len()
}
}
Copy link
Member Author

Choose a reason for hiding this comment

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

These changes are because I was annoyed that I had to explicitly call self.declarations.iter() in order to iterate over self.declarations, rather than just directly iterating over &self.declarations 😄

@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 29, 2026

Typing conformance results

The percentage of diagnostics emitted that were expected errors decreased from 81.39% to 81.34%. The percentage of expected errors that received a diagnostic increased from 71.60% to 71.79%.

Summary

Metric Old New Diff Outcome
True Positives 774 776 +2 ⏫ (✅)
False Positives 177 178 +1 ⏫ (❌)
False Negatives 307 305 -2 ⏬ (✅)
Total Diagnostics 951 954 +3
Precision 81.39% 81.34% -0.05% ⏬ (❌)
Recall 71.60% 71.79% +0.19% ⏫ (✅)

True positives added

Details
Location Name Message
historical_positional.py:26:16 invalid-legacy-positional-parameter Invalid use of the legacy convention for positional-only parameters: Parameter name begins with __ but will not be treated as positional-only
historical_positional.py:38:26 invalid-legacy-positional-parameter Invalid use of the legacy convention for positional-only parameters: Parameter name begins with __ but will not be treated as positional-only

False positives added

Details
Location Name Message
historical_positional.py:29:28 invalid-legacy-positional-parameter Invalid use of the legacy convention for positional-only parameters: Parameter name begins with __ but will not be treated as positional-only

@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 29, 2026

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 29, 2026

mypy_primer results

Changes were detected when running on open source projects
bidict (https://github.com/jab/bidict)
+ bidict/_orderedbase.py:51:40: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
- Found 18 diagnostics
+ Found 19 diagnostics

more-itertools (https://github.com/more-itertools/more-itertools)
+ more_itertools/more.pyi:895:5: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
+ more_itertools/more.pyi:901:5: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
- Found 35 diagnostics
+ Found 37 diagnostics

beartype (https://github.com/beartype/beartype)
+ beartype/_util/cache/map/utilmaplru.py:128:9: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
+ beartype/_util/cache/map/utilmaplru.py:129:9: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
+ beartype/_util/cache/map/utilmaplru.py:130:9: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
+ beartype/_util/cache/map/utilmaplru.py:131:9: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
+ beartype/_util/cache/map/utilmaplru.py:176:9: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
+ beartype/_util/cache/map/utilmaplru.py:177:9: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
+ beartype/_util/cache/map/utilmaplru.py:178:9: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
+ beartype/_util/cache/map/utilmaplru.py:179:9: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
+ beartype/_util/cache/map/utilmaplru.py:180:9: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
+ beartype/_util/cache/map/utilmaplru.py:213:9: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
+ beartype/_util/cache/map/utilmaplru.py:214:9: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
+ beartype/_util/cache/map/utilmaplru.py:215:9: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
+ beartype/_util/cache/map/utilmaplru.py:216:9: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
- Found 502 diagnostics
+ Found 515 diagnostics

psycopg (https://github.com/psycopg/psycopg)
+ psycopg/psycopg/_copy_base.py:400:35: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
+ psycopg/psycopg/_copy_base.py:408:35: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
+ psycopg/psycopg/types/array.py:379:5: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
+ psycopg/psycopg/types/json.py:109:55: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
+ psycopg/psycopg/types/json.py:129:51: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
- Found 653 diagnostics
+ Found 658 diagnostics

setuptools (https://github.com/pypa/setuptools)
+ setuptools/_vendor/more_itertools/more.pyi:888:5: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
+ setuptools/_vendor/more_itertools/more.pyi:894:5: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
- Found 1143 diagnostics
+ Found 1145 diagnostics

pwndbg (https://github.com/pwndbg/pwndbg)
- pwndbg/aglib/kernel/__init__.py:264:20: error[unsupported-operator] Operator `+` is not supported between objects of type `None | Unknown` and `Literal[1]`
+ pwndbg/aglib/kernel/__init__.py:264:20: error[unsupported-operator] Operator `+` is not supported between objects of type `Unknown | None` and `Literal[1]`
- pwndbg/aglib/kernel/__init__.py:270:22: error[unsupported-operator] Operator `+` is not supported between objects of type `None | Unknown` and `int`
+ pwndbg/aglib/kernel/__init__.py:270:22: error[unsupported-operator] Operator `+` is not supported between objects of type `Unknown | None` and `int`

egglog-python (https://github.com/egraphs-good/egglog-python)
+ python/egglog/runtime.py:769:51: warning[invalid-legacy-positional-parameter] Invalid use of the legacy convention for positional-only parameters: Parameter name begins with `__` but will not be treated as positional-only
- Found 1484 diagnostics
+ Found 1485 diagnostics

No memory usage changes detected ✅

@AlexWaygood AlexWaygood force-pushed the alex/pre-pep570-lint branch 2 times, most recently from 6cda680 to d1f51de Compare January 29, 2026 14:13
f"--python-version={python_version}",
"--output-format=gitlab",
"--ignore=assert-type-unspellable-subtype",
"--error=invalid-legacy-positional-parameter",
Copy link
Member Author

Choose a reason for hiding this comment

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

this is necessary because I made the default severity of the lint Level::Warn rather than Level::Error (since it won't fail at runtime)

Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like we should set up our conformance suite integration such that warnings count as errors for conformance suite purposes in general.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's best done as a separate change, I think

Copy link
Member Author

Choose a reason for hiding this comment

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

For example, the number of false positives we report might go up due to some unused-ignore-comment diagnostics on comments in the spec that aren't meant to be suppression comments at all, such as https://github.com/python/typing/blob/08f929ba70397b5c898a55bbb73c8b4e2e8e4fbb/conformance/tests/directives_type_ignore_file1.py#L11. (Related: astral-sh/ty#881.)

@AlexWaygood
Copy link
Member Author

False positives added

historical_positional.py:29:28 invalid-legacy-positional-parameter Invalid use of the legacy convention for positional-only parameters: Parameter name begins with __ but will not be treated as positional-only

I disagree that this is a false positive, and have queried this in the Python Type Checker Builders Discord. The example looks like this:

def f3(x: int, *args: int, __y: int) -> None: ...  # OK

and the comment a few lines above says:

Consistent with PEP 570 syntax, positional-only parameters cannot appear
after parameters that accept keyword arguments. Type checkers should
enforce this requirement:

But x here accepts keyword arguments, so according to the comment, I think the conformance suite should expect type checkers to emit an error here, not mandate the absence of an error:

>>> def f3(x: int, *args: int, __y: int) -> None: ...
... 
>>> f3(x=42, __y=56)
>>>

@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 29, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-argument-type 0 61 1
invalid-legacy-positional-parameter 24 0 0
possibly-missing-attribute 0 6 2
invalid-parameter-default 0 0 7
invalid-return-type 0 5 1
invalid-assignment 0 0 1
invalid-await 0 1 0
Total 24 73 12

Full report with detailed diff (timing results)

@AlexWaygood
Copy link
Member Author

Most of the ecosystem hits appear to be true positives. I almost wonder whether this lint should be disabled by default. It's mandated by the spec, but it seems pretty pedantic to me.

@AlexWaygood AlexWaygood marked this pull request as ready for review January 29, 2026 14:46
}

if literal.is_staticmethod(db) {
if literal.is_staticmethod(db) && literal.name(db) != "__new__" {
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible for a method to be a static method, named __new__, but not be the "special" __new__? I.e., is this check too broad? (I assume not?)

Copy link
Member Author

@AlexWaygood AlexWaygood Jan 29, 2026

Choose a reason for hiding this comment

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

I mean... it's Python, so who knows whether it's possible 😆 But no, I don't think that's something that's realistic for us to worry about or model (and I can't think of a way you could make it happen)

Copy link
Member

@charliermarsh charliermarsh left a comment

Choose a reason for hiding this comment

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

TIL, never seen this before. I agree with you that this could plausibly be disabled by default.

@AlexWaygood AlexWaygood force-pushed the alex/pre-pep570-lint branch 2 times, most recently from 5ef34c9 to b20e8c6 Compare January 30, 2026 14:15
Comment on lines +633 to +637
let Some(Type::FunctionLiteral(function_type)) =
infer_definition_types(db, *definition).undecorated_type()
else {
continue;
};
Copy link
Member Author

@AlexWaygood AlexWaygood Jan 30, 2026

Choose a reason for hiding this comment

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

It feels a bit silly to be calling infer_definition_types for a definition inside this scope, but I think that's the only way to get back the undecorated type of a function that was defined in this scope.

We could also add a raw_signatures: FxHashMap<&'ast ast::StmtFunctionDef, Signature<'db>> field to the TypeInferenceBuilder that could be used to query this information. But I think the infer_definition_types call here should be fairly cheap -- since the definition has already been inferred at this point, it should always be present in the Salsa cache.

Cc. @carljm in case you have opinions!

Copy link
Contributor

Choose a reason for hiding this comment

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

This is totally fine, calling a cached Salsa function is very cheap.

f"--python-version={python_version}",
"--output-format=gitlab",
"--ignore=assert-type-unspellable-subtype",
"--error=invalid-legacy-positional-parameter",
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like we should set up our conformance suite integration such that warnings count as errors for conformance suite purposes in general.

@AlexWaygood AlexWaygood enabled auto-merge (squash) January 31, 2026 10:10
@AlexWaygood AlexWaygood merged commit 6f1e6cb into main Jan 31, 2026
44 checks passed
@AlexWaygood AlexWaygood deleted the alex/pre-pep570-lint branch January 31, 2026 10:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ecosystem-analyzer ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants