-
-
Notifications
You must be signed in to change notification settings - Fork 378
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Disallow reassignment of function parameters #2128
Comments
Oh wow! Great issue description! Thank you! I have one specific usecase in mind that we need to cover / explain: def some(a: Union[List[str], None] = None):
if a is None:
a = [] What do you think about it? It is very common. |
I looked back at the original ESLint rule proposal and someone asked a similar question, eslint/eslint#1599 (comment) but it was just described as a code smell with no proper code alternative suggestion. The AirBnB JavaScript style guide also inherits // really bad
function handleThings(opts) {
// No! We shouldn’t mutate function arguments.
// Double bad: if opts is falsy it'll be set to an object which may
// be what you want but it can introduce subtle bugs.
opts = opts || {};
// ...
}
// still bad
function handleThings(opts) {
if (opts === void 0) {
opts = {};
}
// ...
}
// good
function handleThings(opts = {}) {
// ...
} Or assign the parameter to a new local variable, https://airbnb.io/javascript/#functions--reassign-params // bad
function f1(a) {
a = 1;
// ...
}
function f2(a) {
if (!a) { a = 1; }
// ...
}
// good
function f3(a) {
const b = a || 1;
// ...
}
function f4(a = 1) {
// ...
} The first option is that you could use the For your example, it would probably easiest to put the default value in the function signature: def some(a: Union[List[str], None] = []):
pass
some() # -> []
some(None) # -> []
some(["a", "b", "c"]) # -> ["a", "b", "c"] But I realize there is more validation and default fetching code in real-life scenarios. Here is a real example that I've refactored not to violate the rule, Original: async def build(
self,
prev_event_ids: List[str],
auth_event_ids: Optional[List[str]],
depth: Optional[int] = None,
) -> EventBase:
if auth_event_ids is None:
state_ids = await self._state.get_current_state_ids(
self.room_id, prev_event_ids
)
auth_event_ids = self._event_auth_handler.compute_auth_events(
self, state_ids
)
# other code... Updated: might be better to name the parameter as async def build(
self,
prev_event_ids: List[str],
input_auth_event_ids: Optional[List[str]],
depth: Optional[int] = None,
) -> EventBase:
auth_event_ids = input_auth_event_ids
if auth_event_ids is None:
state_ids = await self._state.get_current_state_ids(
self.room_id, prev_event_ids
)
auth_event_ids = self._event_auth_handler.compute_auth_events(
self, state_ids
)
# other code... In this case, I'm really wishing I could also use a JavaScript example using async function build(prevEventIds, inputAuthEventIds, depth) {
let computedAuthEventIds;
if (!inputAuthEventIds) {
state_ids = await _state.get_current_state_ids(
self.room_id, prev_event_ids
);
computedAuthEventIds = _event_auth_handler.compute_auth_events(
self, state_ids
);
}
const authEventIds = inputAuthEventIds || computedAuthEventIds;
// other code...
} This new rule I'm proposing would catch the original bug from the issue description but that's just because we don't need any of this fancy defaulting logic for it. |
I've just learned about PEP 591 which adds
This works great for patching over the last loophole of re-assigning the local variable unexpectedly! from typing import Final
async def build(
self,
prev_event_ids: List[str],
input_auth_event_ids: Optional[List[str]],
depth: Optional[int] = None,
) -> EventBase:
if input_auth_event_ids is None:
state_ids = await self._state.get_current_state_ids(
self.room_id, prev_event_ids
)
computed_auth_event_ids = self._event_auth_handler.compute_auth_events(
self, state_ids
)
auth_event_ids: Final = input_auth_event_ids if input_auth_event_ids is None else computed_auth_event_ids
# other code... In terms of using
|
def some(a: Union[List[str], None] = []):
pass
some() # -> []
some(None) # -> []
some(["a", "b", "c"]) # -> ["a", "b", "c"] This is not the best practice, see: https://florimond.dev/en/posts/2018/08/python-mutable-defaults-are-the-source-of-all-evil/ This use-case is the only concern I have for now, we need to figure out how to handle it. |
Thanks for the great context around this! Such an easy pitfall. We could do the same parameter rename, def some(inputA: Union[List[str], None] = None):
if inputA is None:
a = [] There is a related issue where someone wanted to allow re-assigning if it had the default value: eslint/eslint#14189. Maybe we could just allow re-assigning if the default is Or as already mentioned, an explicit allow with def some(a: Union[List[str], None] = None):
if a is None:
# noqa: WPS4XX
a = [] Do you have any preferences/suggestions? |
We can't do it. Here are two example:
class NotOurClass(object):
def method(self, a: Union[List[str], None] = None):
"""default impl"""
class OurClass(NotOurClass):
def method(self, a: Union[List[str], None] = None):
if a is None:
a = []
# our own impl
|
I'm running out of my known ideas here. Do you see any way out? Is this one an option?
|
There are other corner cases:
So, I really like this idea, but I need to think about the possible limitations. |
Sounds great, please link any discussions 🙂 I was curious on why the limitation was there in the first place but all I have found is from the PEP 591 PR on GitHub. I'm not finding the pre discussion in the mailing list about it though (mailing list noob).
I'm not sure what is meant there 🤔 Related mailing list threads I could find but not what I was after: |
Created python/mypy#11076 to propose it in on the In
In a previous revision of the documentation, it mentioned this example:
|
Rule request
Thesis
Add a lint rule similar to ESlint
no-param-reassign
for JavaScript which disallows reassignment of function parameters.When a function parameter gets re-assigned, it masks the original argument passed in. With a sufficiently long function, it's not obvious that the variable was assigned to something different than what you expect from quickly reading the function signature then jumping down to the relevant piece you're interested in. When this occurs, it's probably more the case, that you accidentally used the same variable name which is why it should be disallowed.
Even if re-assigning the function parameter was intended, it would probably be more clear to assign a new local variable and maybe make a copy to avoid modifying the underlying object:
Reasoning
There are similar violations already in the best practices section but they don't cover the following scenario.
BlockAndLocalOverlapViolation
: Forbid overlapping local and block variables.OuterScopeShadowingViolation
: Forbid shadowing variables from outer scopes.ControlVarUsedAfterBlockViolation
: Forbid control variables after the block body.Recently ran into a real-life bug because of accidentally re-assigning a function parameter, https://github.com/matrix-org/synapse/pull/10439/files/a94217ee34840237867d037cf1133f3a9bf6b95a
Simplified reproduction case:
Fixed code:
This rule wouldn't protect from a similar scenario where
context
is a local variable because Python does not have block scope and there is no distinction between declaration and assignment so we can't be protected that way either. I don't think we can solve this scenario with lints unless the linter wants to interpret Python and enforce block-scope with a unique variable name constraint (not suggesting this).Comparing to JavaScript
How the same code would behave in JavaScript
This isn't trying to claim JavaScript vs Python. I'm just trying to document what I am familiar with and come up with some lints we can use for Python to avoid the same scenarios.
Converting that problem Python code to JavaScript, it works as expected even though the function parameter and variable in the loop are both named
context
because JavaScript is block-scoped. Theconst
usage can also be enforced with ESLintsprefer-const
rule.If we forget the
const
, ESLint warns us about re-assigning a function parameter thanks to the ESlintno-param-reassign
rule.(ESLint demo)
If we change the example where the
context
is already a local variable in the function instead of a function parameter, it still works because of block scope.And finally, if we forget the
const
, on the secondcontext
, we seeUncaught TypeError: Assignment to constant variable.
because of the niceconst
guarantees (can't be reassigned or redeclared).The text was updated successfully, but these errors were encountered: