single-letter-name (SMP101)

SMP101No fixNamingAll rules

Flags single-letter names outside comprehensions.

Why is this bad?

A single letter says nothing about what the name holds, so the reader has to find where it is bound to learn it. Comprehensions are exempt because the iterable the names come from sits right next to them.

Example

Flagged
1def scale(v: float, f: float) -> float:
2    r = v * f
3    return round(r, 2)
Preferred
1def scale(value: float, factor: float) -> float:
2    scaled = value * factor
3    return round(scaled, 2)

What it does

No fix is offered, since a meaningful name cannot be derived from the code.

Active on

  • Functions and classes: their names show up at every call site, far from the definition that explains them.

    Example: reported
    1def f() -> int:
    2    return 1
    
  • Parameters: the body reads them away from the signature, and callers see the name in the signature and keyword arguments. This covers:

    • Parameters of a def, such as v in def scale(v: float):

      Example: reported
      1def scale(v: float) -> float:
      2    return v * 2
      
    • Parameters of a lambda, such as v in lambda v: v:

      Example: reported
      1callback = lambda v: v
      
  • Assignment and walrus targets: the value is computed once, and every later use relies on the name alone to say what it is.

    • = assignments, such as r = 3:

      Example: reported
      1r = 3
      2print(r)
      
    • := walrus targets, such as n in if (n := 3) > 2:

      Example: reported
      1if (n := 3) > 2:
      2    print(n)
      
  • with ... as, except ... as and import ... as names: the alias replaces the expression, exception or module name that said what the value is.

    • with ... as names, such as f in with open("data.txt") as f:

      Example: reported
      1with open("data.txt") as f:
      2    print(f.read())
      
    • except ... as names, such as e in except ValueError as e:

      Example: reported
      1try:
      2    run()
      3except ValueError as e:
      4    print(e)
      
    • import ... as names, such as o in import os as o:

      Example: reported
      1import os as o
      
  • match captures: the case body reads the captured value under that name.

    Example: reported

    pyproject.toml

    1[project]
    2requires-python = ">=3.12"
    
    1value = 1
    2match value:
    3    case x:
    4        print(x)
    
  • Mapping rest captures: **rest in a mapping pattern binds the remaining keys, and the case body reads them under that name.

    Example: reported
    1value = {"a": 1, "b": 2}
    2match value:
    3    case {"a": 1, **r}:
    4        print(r)
    
  • Type parameters such as T: they appear throughout the signature and body of the generic definition.

    Example: reported

    pyproject.toml

    1[project]
    2requires-python = ">=3.12"
    
    1def first[T](items: list[T]) -> T:
    2    return items[0]
    
  • for loop targets, such as i in for i in range(3): a loop body can run for many lines, far from the for line that says what the name holds.

    Example: reported
    1for i in range(3):
    2    print(i)
    

Exceptions

  • Names inside a comprehension: every name inside a list, set or dictionary comprehension or a generator expression, including a walrus target or lambda parameter, so [x * 2 for x in values] is fine. The whole expression keeps the iterable next to every use of the name.

    Example: not reported
    1values = [1, 2]
    2doubled = [x * 2 for x in values]
    
  • _: the conventional throwaway name, which says the value is unused.

    Example: not reported
    1for _ in range(3):
    2    pass
    
  • Parameters of a method decorated with @override: the method must keep the parameter names of the method it replaces.

    Example: not reported

    pyproject.toml

    1[project]
    2requires-python = ">=3.12"
    
     1from typing import override
     2
     3
     4class Base:
     5    def run(self, value: int) -> None:
     6        pass
     7
     8
     9class Child(Base):
    10    @override
    11    def run(self, v: int) -> None:
    12        pass
    
  • l, I and O: Ruff’s ambiguous-variable-name, ambiguous-class-name and ambiguous-function-name report these letters wherever this rule would, so this rule leaves them to Ruff while the project’s Ruff configuration selects the rule for the kind of binding. Ruff’s own suppressions of it, through per-file-ignores or noqa, are respected too.

    Example: not reported

    pyproject.toml

    1[tool.ruff.lint]
    2select = ["E741"]
    
    1l = 3
    

Reporting

Each name is reported once per scope, at its first binding.

Ruff overlap

Ruff’s E741, E742 and E743 report l, I and O on variables, parameters, except names, classes and functions, with no fix. This rule leaves those three letters to Ruff at those sites, and only while the project’s Ruff configuration selects the rule of each kind. Ruff’s own suppressions of it, through per-file-ignores or noqa, are respected too. It keeps every other single letter, and the l, I and O that Ruff does not check: import aliases, match captures and type parameters.

Other tools

  • Ruff: variation of released rules E741, E742 and E743, which report l, I and O and which this rule leaves to them; issue #4866 asking for all single letters was closed as not planned.

  • flake8-variables-names reports single-letter names as VNE001, but allows i, _ and T by default. This rule flags for i loop targets and type parameters such as T.

  • wemake-python-styleguide reports short names as WPS111, with a configurable minimum length. It also checks module and attribute names, which this rule does not list.