Skip to content

Require an automatic parameter name to be a Python identifier - #3827

Open
kdeldycke wants to merge 1 commit into
pallets:stablefrom
kdeldycke:param-name-case-coverage
Open

Require an automatic parameter name to be a Python identifier#3827
kdeldycke wants to merge 1 commit into
pallets:stablefrom
kdeldycke:param-name-case-coverage

Conversation

@kdeldycke

@kdeldycke kdeldycke commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

This PR changes the behavior of automatic naming:

  • An argument must produce a valid Python identifier. 0-file, foo.bar and foo bar are now refused.
  • expose_value=False no longer bypasses that check, on both arguments and options.
  • An argument or option requires at least one declaration that can't be empty. click.argument(expose_value=False) and click.option(expose_value=False) used to produce a "" parameter.

What does not change:

  • The transformation itself: every - becomes _ and the result is lower cased.
  • An exposed option still raise if not producing a Python identifier, like click.option("--0-file").
  • An awkward explicit option name is not normalized: click.option("--in-file", "Input_File") still names its parameter Input_File.

How to migrate:

  • Give an option an explicit name: click.option("--0-file", "zero_file").
  • Rename an argument and pass metavar to keep the old display: click.argument("zero_file", metavar="0-FILE").

This PR also lockdown all other edge-cases and canonical naming behavior.

It is in the same vein as my previous PR at #3808 , and is also based on a collection of edge-cases I accumulated over the years. It also covers platform differences between Windows and Unix-like (see: #2483).

Note: this PR started as simple test coverage expansion but ended up implementing sanitizing fixes.

@kdeldycke kdeldycke added this to the 8.5.1 milestone Sep 1, 2026
@kdeldycke kdeldycke added docs Updates to documentation, readme, docstrings, typos tests Click's own test suite and CI workflows labels Sep 1, 2026
@davidism

davidism commented Sep 1, 2026

Copy link
Copy Markdown
Member

Both of these seem like bugs. I feel like the automatic names should always be normalized and pass str.isidentifier.

@kdeldycke
kdeldycke force-pushed the param-name-case-coverage branch from 254d9e4 to 49a5f5a Compare September 1, 2026 14:43
@kdeldycke

Copy link
Copy Markdown
Collaborator Author

Both of these seem like bugs. I feel like the automatic names should always be normalized and pass str.isidentifier.

OK cool, that was also my feeling. We don't want special treatments of the name of parameters, whatever their kind (options or arguments). I cannot find any reasons with the transformation should be different between the two kinds. So in the end yes, str.isidentifier should be the minimal check common to the two.

@kdeldycke kdeldycke changed the title Cover edge-cases of parameter's naming WIP: Cover edge-cases of parameter's naming Sep 1, 2026
@kdeldycke
kdeldycke marked this pull request as draft September 1, 2026 14:44
@kdeldycke
kdeldycke force-pushed the param-name-case-coverage branch from 49a5f5a to ad8601c Compare September 1, 2026 22:15
@kdeldycke kdeldycke changed the title WIP: Cover edge-cases of parameter's naming WIP: Require Argument's naming to be a Python identifier Sep 1, 2026
@kdeldycke kdeldycke changed the title WIP: Require Argument's naming to be a Python identifier WIP: Require an Argument name to be a Python identifier Sep 1, 2026
@kdeldycke

Copy link
Copy Markdown
Collaborator Author

Both of these seem like bugs. I feel like the automatic names should always be normalized and pass str.isidentifier.

@davidism Done. I did not pushed the normalization too much.

So there is one bothering difference between Argument and Option: the latter makes click.option("--in-file", "Input_File") names its parameter Input_File, while Argument forces it to input_file. Should I align both? I would tend to yes.

@kdeldycke kdeldycke changed the title WIP: Require an Argument name to be a Python identifier Require an Argument name to be a Python identifier Sep 1, 2026
@kdeldycke
kdeldycke force-pushed the param-name-case-coverage branch 3 times, most recently from 05c4439 to 3d4142f Compare September 2, 2026 11:03
@kdeldycke kdeldycke changed the title Require an Argument name to be a Python identifier Sanitize Argument and Option automatic naming Sep 2, 2026
@kdeldycke

Copy link
Copy Markdown
Collaborator Author

I just pushed the alignment and normalization of naming a bit more and updated the differences in the body of that PR.

@kdeldycke
kdeldycke force-pushed the param-name-case-coverage branch from 3d4142f to b4606f9 Compare September 2, 2026 12:43
@kdeldycke kdeldycke changed the title Sanitize Argument and Option automatic naming Require an automatic parameter name to be a Python identifier Sep 2, 2026
@kdeldycke
kdeldycke force-pushed the param-name-case-coverage branch from b4606f9 to ff79a3c Compare September 2, 2026 13:11
@kdeldycke
kdeldycke marked this pull request as ready for review September 2, 2026 13:29
@kdeldycke

Copy link
Copy Markdown
Collaborator Author

OK these were my final edits. You can review this PR.

@kdeldycke
kdeldycke force-pushed the param-name-case-coverage branch from ff79a3c to 6f9943e Compare September 2, 2026 13:32
Comment thread docs/arguments.md
Comment on lines +112 to +121
```{caution}
Only the ASCII hyphen is replaced, so a separator that merely looks like one is
refused, as is any character that renders nothing.

The identifier set itself moves with the Unicode table Python ships: the
zero-width joiner (`U+200D`) entered it in Python 3.13, so a declaration holding
one is refused up to Python 3.12 and names a parameter from 3.13 on. Prefer a
declaration that is already a lower-case identifier with `-` for `_`.
```

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.

This is probably too much detail. I doubt anyone's writing these, but linking to isidentifier should already be enough.

Comment thread docs/options.md
Comment on lines +104 to 140
(keyword-names)=

```{caution}
A [reserved keyword](https://docs.python.org/3/reference/lexical_analysis.html#keywords)
satisfies that check, so Click accepts one: `click.option("--from")` names its
parameter `from`. Three things follow, and Click reports none of them.

- The callback cannot declare it. `def cmd(from)` is a {exc}`SyntaxError`, so
the command has to take `**kwargs` instead.
- That `**kwargs` then covers every other parameter too, and Python stops
checking the callback signature. An option you rename or drop used to raise
`TypeError: got an unexpected keyword argument`, and is now absorbed in
silence.
- {meth}`Context.invoke` cannot name it either. Write
`ctx.invoke(other, **{"from": value})`, because `ctx.invoke(other, from=value)`
is a {exc}`SyntaxError`. {meth}`Context.forward` is unaffected, since it
unpacks {attr}`Context.params`.

Pass an explicit name instead: `click.option("--from", "source")`. An argument
takes one declaration and has no explicit-name channel, so rename the
declaration there. Soft keywords (`match`, `case`, `type`, `_`) are contextual
and name a parameter fine, and `--True`, `--False` and `--None` lower case out
of the keyword set.
```

`expose_value=False` is no exception. The name is also the parser dest the value
is stored under, so two options that gave it up would share that dest and each
read the other's value. Pass an explicit name instead:
`click.option("--0-file", "zero_file", expose_value=False)`.

```{caution}
Transformation from option name to argument name is not reversible. And is many-to-one: several option names can map to the same argument name.

For example, `--foo-bar`, `--Foo-Bar` and `--FOO-BAR` all map to `foo_bar`.

This is allowed so that options can deliberately form a [feature switch group](#feature-switch-group).
```

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.

This also seems like too much detail.

The example at the end about capitalization can be added to the table at the top if it's not there.

Comment thread CHANGES.md
- `expose_value=False` no longer excuses that check on either kind. Pass an
explicit name to {class}`Option`, or rename an {class}`Argument` and pass
`metavar` to keep its display. {pr}`3827`
- Neither kind builds a parameter without a declaration. `click.argument()` and

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.

Is this saying that Parameter.name can no longer be None? Do we need to update the annotation? Would be great if so.

@davidism

davidism commented Sep 2, 2026

Copy link
Copy Markdown
Member

This needs to be targeted at 9.0 and listed high up in the changelog, the name normalization is a breaking change. Along with the keyword check I mention below, this could be significantly disruptive. We should probably issue a deprecation warning and accept names that are currently accepted, before applying the new rules.

It might be good for the TypeError to give explicit instructions: Argument {decls} tried to use 'name' but it is not a valid Python identifier. Add a valid name to the parameter declaration.

Overall I think the docs are a little too verbose about this. Giving a few examples and linking to isidentifier should be enough.

You bring up a good point about keywords. I think the intention of this code was clear: to only accept names that can be used in signatures rather than kwargs. We can also check that the name isn't a keyword with https://docs.python.org/3/library/keyword.html. I bet from and in are pretty common option names though, perhaps this is not worth it?

Comment thread src/click/core.py

raise TypeError(
_(
"Could not determine name for {param_type} with declarations {decls!r}"

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.

"Could not determine a valid Python identifier for param_type decls."

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Updates to documentation, readme, docstrings, typos tests Click's own test suite and CI workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing auto-generated environment variables in help screen & case-sensitivity

2 participants