question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Disallow reassignment of function parameters

See original GitHub issue

Rule request

Thesis

Add a lint rule similar to ESlint no-param-reassign for JavaScript which disallows reassignment of function parameters.

Assignment to variables declared as function parameters can be misleading and lead to confusing behavior, as modifying function parameters will also mutate the arguments object. Often, assignment to function parameters is unintended and indicative of a mistake or programmer error.

https://eslint.org/docs/rules/no-param-reassign

Why? Manipulating objects passed in as parameters can cause unwanted variable side effects in the original caller.

https://airbnb.io/javascript/#functions--mutate-params

It’s also about the clarity of mutating and reassigning - it’s easier to understand functions when the arguments are static and constant throughout the life of the function.

https://github.com/eslint/website/issues/686#issuecomment-577419721 Related discussion, https://github.com/eslint/eslint/issues/5306

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:

def example(context):
  new_context = context
  if True:
    new_context = compute_context()
  • shadowing
  • masking
  • overriding
  • overwriting

Reasoning

There are similar violations already in the best practices section but they don’t cover the following scenario.


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:

def ensure_correct_context(event, context):
  remote_auth_chain = get_event_auth(event)
  for auth_event in remote_auth_chain:
    # XXX: Violation should trigger here!
    # We are introducing a bug because `context` which corresponds to `event`
    # is being re-assigned to the `context` for the `auth_event`
    context = compute_event_context(auth_event)
    persist_event(auth_event, context)

  # logic for context_needs_updating

  if context_needs_updating:
    return compute_event_context(event)

  return context

Fixed code:

def ensure_correct_context(event, context):
  remote_auth_chain = get_event_auth(event)
  for auth_event in remote_auth_chain:
    auth_event_context = compute_event_context(auth_event)
    persist_event(auth_event, auth_event_context)

  # logic for context_needs_updating

  if context_needs_updating:
    return compute_event_context(event)

  return context

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

def ensure_correct_context(event):
  context = compute_event_context(event)
  
  remote_auth_chain = get_event_auth(event)
  for auth_event in remote_auth_chain:
    # XXX: Bug is here but no violation would be triggered
    context = compute_event_context(auth_event)
    persist_event(auth_event, context)

  # logic for context_needs_updating

  if context_needs_updating:
    return compute_event_context(event)

  return context

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. The const usage can also be enforced with ESLints prefer-const rule.

'use strict';

function get_event_auth() { return [1,2,3]; }
function compute_event_context(event) { return { foo: event } }
function persist_event() {}

function ensure_correct_context(event, context) {
  const remote_auth_chain = get_event_auth(event);
  for (const auth_event of remote_auth_chain) {
    const context = compute_event_context(auth_event);
    persist_event(auth_event, context);
  }

  // logic for context_needs_updating
  const context_needs_updating = false;

  if (context_needs_updating) {
    return compute_event_context(event);
  }

  return context;
}

ensure_correct_context(999, { foo: 999 })
// -> {foo: 999}
// ✅ expected result

If we forget the const, ESLint warns us about re-assigning a function parameter thanks to the ESlint no-param-reassign rule.

(ESLint demo)

'use strict';

function get_event_auth() { return [1,2,3]; }
function compute_event_context(event) { return { foo: event } }
function persist_event() {}

function ensure_correct_context(event, context) {
  const remote_auth_chain = get_event_auth(event);
  for (const auth_event of remote_auth_chain) {
    context = compute_event_context(auth_event);
    persist_event(auth_event, context);
  }

  // logic for context_needs_updating
  const context_needs_updating = false;

  if (context_needs_updating) {
    return compute_event_context(event);
  }

  return context;
}

ensure_correct_context(999, { foo: 999 })
// -> ESLint: 10:5 - Assignment to function parameter 'context'.
// ✅ The linter caught our mistake

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.

'use strict';

function get_event_auth() { return [1,2,3]; }
function compute_event_context(event) { return { foo: event } }
function persist_event() {}

function ensure_correct_context(event) {
  const context = compute_event_context(event);

  const remote_auth_chain = get_event_auth(event);
  for (const auth_event of remote_auth_chain) {
    const context = compute_event_context(auth_event);
    persist_event(auth_event, context);
  }

  // logic for context_needs_updating
  const context_needs_updating = false;

  if (context_needs_updating) {
    return get_new_context_for_event(event);
  }

  return context;
}

ensure_correct_context(999)
// -> {foo: 999}
// ✅ expected result

And finally, if we forget the const, on the second context, we see Uncaught TypeError: Assignment to constant variable. because of the nice const guarantees (can’t be reassigned or redeclared).

'use strict';

function get_event_auth() { return [1,2,3]; }
function compute_event_context(event) { return { foo: event } }
function persist_event() {}

function ensure_correct_context(event) {
  const context = compute_event_context(event);

  const remote_auth_chain = get_event_auth(event);
  for (const auth_event of remote_auth_chain) {
    context = compute_event_context(auth_event);
    persist_event(auth_event, context);
  }

  // logic for context_needs_updating
  const context_needs_updating = false;

  if (context_needs_updating) {
    return get_new_context_for_event(event);
  }

  return context;
}

ensure_correct_context(999)
// -> Uncaught TypeError: Assignment to constant variable.
// ✅ The language mechanics caught our mistake

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:10 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
sobolevncommented, Aug 3, 2021

There are other corner cases:

So, I really like this idea, but I need to think about the possible limitations. I will try to ask mypy team about Final type on arguments. Maybe this is the way to go.

0reactions
MadLittleModscommented, Sep 8, 2021

Created https://github.com/python/mypy/issues/11076 to propose it in on the mypy side. Also found a bit more discussion there around the original limitation.

In mypy, I could find that Final was added in https://github.com/python/mypy/pull/5522 which mentions:

I don’t allow Final in function argument types. One argument is simplicity, another is I didn’t see many bugs related to shadowing an argument in function bodies, finally people might have quite different expectations for this. If people will ask, this would be easy to implement.

https://github.com/python/mypy/pull/5522

In a previous revision of the documentation, it mentioned this example:

  • Final can be only used as an outermost type in assignments, using it in any other position is an error. In particular, Final can’t be used in annotations for function arguments because this may cause confusions about what are the guarantees in this case:
: List[Final[int]] = []  # Error!
def fun(x: Final[List[int]]) ->  None:  # Error!

https://github.com/python/mypy/pull/5522/files/c9abd9db835240962467a9ee164f4bbb50d56545#diff-3b595f6f83800d78a304cf528ed39aff352c8cd8282edda26bafced79646baad

Read more comments on GitHub >

github_iconTop Results From Across the Web

no-param-reassign - ESLint - Pluggable JavaScript Linter
This rule aims to prevent unintended behavior caused by modification or reassignment of function parameters. Examples of incorrect code for this rule: /*eslint ......
Read more >
How to avoid no-param-reassign when setting a property on a ...
As this article explains, this rule is meant to avoid mutating the arguments object. If you assign to ...
Read more >
Rule no-param-reassign - ESLint中文
Disallow Reassignment of Function Parameters (no-param-reassign) ... rule aims to prevent unintended behavior caused by overwriting function parameters.
Read more >
Eslint no param reassign allow
Disallow Reassignment of Function Parameters (no-param-reassign) Assignment to variables declared as function parameters can be misleading and lead to confusing ...
Read more >
Why eslint throws "Assignment to property of function ... - Reddit
It will allow you to reassign object properties but not the argument reference itself. Or you can disable the rule on specific lines...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found