Skip to content

ruff rule ANN201 missing-return-type-undocumented-public-function - #15296

Merged
cclauss merged 1 commit into
TheAlgorithms:masterfrom
cclauss:ruff-rule-ANN201-unsafe-fixes
Sep 12, 2026
Merged

ruff rule ANN201 missing-return-type-undocumented-public-function#15296
cclauss merged 1 commit into
TheAlgorithms:masterfrom
cclauss:ruff-rule-ANN201-unsafe-fixes

Conversation

@cclauss

@cclauss cclauss commented Sep 12, 2026

Copy link
Copy Markdown
Member

https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function

@dhruvmanila @priya-sundaram-dev, please review and help us understand how to identify which changes are unsafe.

% ruff check --select=ANN201 --statistics

619	ANN201	missing-return-type-undocumented-public-function
Found 619 errors.
No fixes available (260 hidden fixes can be enabled with the `--unsafe-fixes` option).

% ruff check --select=ANN201 --fix --unsafe-fixes --silent

% ruff check --select=ANN201 --statistics

359	ANN201	missing-return-type-undocumented-public-function
Found 359 errors.

% ruff rule ANN201

missing-return-type-undocumented-public-function (ANN201)

Derived from the flake8-annotations linter.

Fix is sometimes available.

What it does

Checks that public functions and methods have return type annotations.

Why is this bad?

Type annotations are a good way to document the return types of functions. They also
help catch bugs when used alongside a type checker by ensuring that the types of
any returned values, and the types expected by callers, match expectations.

Example

def add(a, b):
    return a + b

Use instead:

def add(a: int, b: int) -> int:
    return a + b

Availability

Because this rule relies on the third-party typing_extensions module for some Python versions,
its diagnostic will not be emitted, and no fix will be offered if typing_extensions imports
have been disabled by the [lint.typing-extensions] linter option.

Options

  • lint.typing-extensions

@cclauss
cclauss requested a review from dhruvmanila September 12, 2026 12:57
@cclauss cclauss added the require type hints https://docs.python.org/3/library/typing.html label Sep 12, 2026
@algorithms-keeper algorithms-keeper Bot added awaiting reviews This PR is ready to be reviewed enhancement This PR modified some existing files and removed require type hints https://docs.python.org/3/library/typing.html labels Sep 12, 2026
@cclauss cclauss added the require type hints https://docs.python.org/3/library/typing.html label Sep 12, 2026
@priya-sundaram-dev

Copy link
Copy Markdown
Contributor

Looked into this — the good news is that the ANN201 unsafe autofix is much narrower than "260 unknown changes." It only ever does one thing: add -> None, and only to functions where ruff can statically prove there is no value-returning return/yield. It deliberately leaves alone:

  • functions that return a value (it can't infer the type, so it won't guess), and
  • bodies that raise NotImplementedError / are pure stubs.

Quick repro (isolated, so no repo config interferes):

def implicit_none(x):        # -> gets `-> None`
    if x: print(x)
def bare_return(x):          # -> gets `-> None`
    if not x: return
    print(x)
def base_method(self):       # left ALONE (raises)
    raise NotImplementedError
def returns_value(x):        # left ALONE (returns a value)
    return x + 1
$ ruff check --isolated --select=ANN201 --fix --unsafe-fixes --diff
-def implicit_none(x):
+def implicit_none(x) -> None:
-def bare_return(x):
+def bare_return(x) -> None:
Would fix 2 errors.

So the ~260 fixes that take us 619 → 359 are all -> None additions, and the remaining 359 are genuine "returns a value, ruff won't guess the type" cases that need a human or a type checker.

Why ruff still marks the -> None fix "unsafe" (and where to be careful): it's not that the None inference is wrong — it's that -> None changes the public contract. The one class of real regressions is a base/overridable method: if def load(self): in a base class is currently unannotated and a subclass overrides it to return data, pinning the base to -> None makes the subclass Liskov-inconsistent and a type checker will now flag the subclass. Free functions and main()/CLI entrypoints are safe.

Suggested workflow to land this safely:

  1. ruff check --select=ANN201 --fix --unsafe-fixes to grab all the -> None additions in one branch.
  2. Run the type checker (mypy/ty) on that branch — any newly-added -> None that a checker still accepts on a leaf function is safe to keep.
  3. grep the diff for -> None added to methods (params start with self/cls) and spot-check the ones whose class is subclassed elsewhere — those are the only Liskov risks.
  4. For the remaining 359, split by top-level directory (data_structures/, graphs/, …) into reviewable PRs and annotate the real return types; the checker's inferred types make good starting points.

Happy to take a directory (e.g. data_structures/) as a reference PR if that helps set the pattern.

(Disclosure: I'm Priya Sundaram, an AI software agent; a human reviews my substantive work.)

@cclauss
cclauss requested a review from poyea September 12, 2026 13:29
@algorithms-keeper algorithms-keeper Bot removed the awaiting reviews This PR is ready to be reviewed label Sep 12, 2026
@priya-sundaram-dev

Copy link
Copy Markdown
Contributor

Small accuracy update to my note above, now that I've tested against a current ruff (0.15.21): the ANN201 unsafe autofix is a bit smarter than "only ever adds -> None." It also infers the type of simple literal returns:

def f():            -> def f() -> None:     # no value-returning path
    pass
def g():            -> def g() -> int:      # return 5
    return 5
def h():            -> def h() -> str:      # return "x"
    return "x"
def j(x):           -> (left alone)         # return x  — type not statically known
def k():            -> (left alone)         # raise NotImplementedError

So the ~260 fixable ones aren't all None; they're None plus literal-typed returns ruff could prove. Still safe-by-construction — it never guesses when the return expression's type is unknown. The remaining ~359 value-returning ones (returns a call/variable) are the human-annotation batch. Doesn't change the sequencing I suggested, just wanted the mechanism to be exact.

@cclauss
cclauss merged commit 02716a9 into TheAlgorithms:master Sep 12, 2026
8 checks passed
@cclauss
cclauss deleted the ruff-rule-ANN201-unsafe-fixes branch September 12, 2026 19:57
@cclauss

cclauss commented Sep 12, 2026

Copy link
Copy Markdown
Member Author

In this repo, we are fortunate that there is not much subclassing. The majority of subclassing from things in Python's Standard Library, like Enum, Exception, MutableMapping, NamedTuple, Protocol, TestCase, TypedDict, etc.

% git grep "class " | grep "(" # Filter out the noise...

data_structures/hashing/double_hash.py:class DoubleHash(HashTable):
data_structures/hashing/hash_map.py:class _DeletedItem(_Item):
data_structures/hashing/hash_table_with_linked_list.py:class HashTableWithLinkedList(HashTable):
data_structures/hashing/quadratic_probing.py:class QuadraticProbing(HashTable):
data_structures/linked_list/deque_doubly.py:class LinkedDeque(_DoublyLinkedBase):
geometry/geometry.py:class Circle(Ellipse):
geometry/geometry.py:class Rectangle(Polygon):
geometry/geometry.py:class Square(Rectangle):
graphs/edmonds_karp_multiple_source_and_sink.py:class MaximumFlowAlgorithmExecutor(FlowNetworkAlgorithmExecutor):
graphs/edmonds_karp_multiple_source_and_sink.py:class PushRelabelExecutor(MaximumFlowAlgorithmExecutor):

Please proceed with your data_structures/ pull request.

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

Labels

enhancement This PR modified some existing files require type hints https://docs.python.org/3/library/typing.html

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants