Skip to content
VLSI Mentor

Python Part I · Module 11 · Functions

Type Hints

Chapter 11.11 added a sentence saying what a function promises. A type hint adds the other half of that contract in a form a machine can read: what kind of value each parameter expects, and what kind comes back. You will see them in essentially all modern Python, and the single most important thing to understand about them is what they do not do. Python does not check them. A function can be annotated as returning an integer, return a string instead, and run to completion with no complaint — and this chapter opens by doing exactly that, because a reader who expects a hint to catch mistakes will be caught out by one.

Intermediate15 min readPythonPart IFundamentalsFunctionsType Hints

Part I · Module 11 · Chapter 11.12 · Type Hints

1. The Half a Docstring Does Not Cover

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def count_errors(text_lines):
    """Return the number of lines containing UVM_ERROR."""
    return 0

The sentence says a number comes back. It does not say, in a form anything can act on, that text_lines should be a list of strings rather than one long string — a distinction that produces a completely wrong answer rather than an error, since both are iterable.

2. The Syntax

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def count_errors(text_lines: list[str]) -> int:
    """Return the number of lines containing UVM_ERROR."""
    found = 0
    for line in text_lines:
        if "UVM_ERROR" in line:
            found += 1
    return found


print(count_errors(["UVM_ERROR @1: a", "UVM_INFO @2: b"]))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1

Two pieces:

  • name: type after a parameter — what that parameter expects.
  • -> type after the parameter list — what the function returns.

The behaviour is unchanged. The function does exactly what it did before the annotations were added.

3. Python Does Not Check Them

This is the fact to internalise first:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
def count_errors(text: str) -> int:
    return "not an int"


print(count_errors(4210))
print(type(count_errors(4210)))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
not an int
<class 'str'>

Two violations in one call. The parameter is annotated str and received an integer. The return is annotated int and produced a string. Nothing raised, nothing warned.

So why write them? Section 8. First, where they go.

4. They Are Stored on the Function

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def count_errors(text: str) -> int:
    return 0


print(count_errors.__annotations__)
print(type(count_errors.__annotations__))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{'text': <class 'str'>, 'return': <class 'int'>}
<class 'dict'>

A dictionary, keyed by parameter name, with "return" for the return type. That is the whole mechanism — and it is why external tools can read them, which is the entire value proposition in Section 8.

5. The Common Scalar Types

The built-in type names are the annotations:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def format_job(name: str, seed: int, timeout: float, waves: bool) -> str:
    return name + "/" + str(seed) + "/" + str(timeout) + "/" + str(waves)


print(format_job("smoke", 4210, 1.5, True))
print(format_job.__annotations__)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
smoke/4210/1.5/True
{'name': <class 'str'>, 'seed': <class 'int'>, 'timeout': <class 'float'>, 'waves': <class 'bool'>, 'return': <class 'str'>}

str, int, float, bool — the types from Module 2, used as annotations with no new spelling to learn.

6. Collections Say What They Contain

A bare list is nearly useless as documentation; square brackets say what is inside:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def names(items: list[str]) -> dict[str, int]:
    return {"count": len(items)}


print(names(["a", "b"]))
print(names.__annotations__)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{'count': 2}
{'items': list[str], 'return': dict[str, int]}
  • list[str] — a list of strings
  • dict[str, int] — a dictionary with string keys and integer values
  • tuple[str, int] — a two-field tuple of a string and an integer
  • set[str] — a set of strings

That last shape is worth pairing with Chapter 11.9: a function returning total, passed is annotated -> tuple[int, int], which tells a reader how many values to unpack as well as what they are.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def summarise(results: list[str]) -> tuple[int, int]:
    total = len(results)
    passed = 0
    for status in results:
        if status == "PASS":
            passed += 1
    return total, passed


total, passed = summarise(["PASS", "FAIL", "PASS"])
print(total, passed)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
3 2

7. -> None for a Function With No Result

A function that exists for its effect returns None, and saying so is useful:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def report(message: str, loud: bool = False) -> None:
    print(message.upper() if loud else message)


report("starting")
report("starting", loud=True)
print(report.__annotations__)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
starting
STARTING
{'message': <class 'str'>, 'loud': <class 'bool'>, 'return': None}

Note that a default and an annotation coexist: loud: bool = False reads a boolean, defaulting to False.

-> None is not a technicality. It is the annotation that tells a reader do not use the result of this call — which is Chapter 11.8's distinction, written into the signature.

8. What They Are Actually For

Three concrete returns, none of which is run-time safety.

Editors. Annotate a parameter as list[str] and your editor knows that text_lines[0] is a string, so it offers .strip() and .startswith() and flags .append() on it. That completion is the benefit most people notice first and it is immediate.

A separate checking tool. Programs exist whose job is to read annotations across a whole codebase and report mismatches before anything runs — passing an int where str was declared, or using a result annotated -> None. They are not part of Python and are not installed by default; setting one up belongs to a project's tooling rather than to this course. The point for now is that the checking is done by a tool, not by Python, which is exactly why Section 3 behaves as it does.

Review and design. The most underrated one. A parameter you cannot name a type for is usually a parameter doing two jobs, and a function annotated -> dict that sometimes returns None has an inconsistency its author had not noticed. Writing the annotation forces the question.

9. When to Leave Them Out

Hints are not free — they are text to read and to keep correct:

  • A three-line helper whose types are obvious. def is_error(line): return "UVM_ERROR" in line gains little.
  • Anywhere the annotation would be a guess. A wrong hint misleads exactly like a wrong docstring, and for the same reason: a reader will believe it.
  • Exploratory code you are still shaping. Add them once the signature settles.

The strong cases are the mirror image: functions other people call, functions whose argument types are not obvious from their names, and anything returning a collection, where -> dict[str, int] saves the reader from inferring the shape by reading the body.

10. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# A
def a(x: int) -> int:
    return x * 2
print("A:", a(3), a("ab"))

# B
def b() -> None:
    return 7
print("B:", b())

# C
def c(items: list[str]) -> int:
    return len(items)
print("C:", c(["x", "y"]), c.__annotations__["return"])

# D
def d(n: int = 5) -> str:
    return str(n)
print("D:", d(), d(9), d.__annotations__)

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: 6 abab
B: 7
C: 2 <class 'int'>
D: 5 9 {'n': <class 'int'>, 'return': <class 'str'>}

A — a("ab") was annotated int and given a string. It ran, and "ab" * 2 is "abab". The annotation changed nothing.

B — annotated -> None and returns 7. Still nothing.

C — annotations are readable as data, which is how tools use them.

D — a default and an annotation on the same parameter, both recorded.

A and B are the chapter in two lines: the hints were wrong and the program did not care.

11. Exercises

Exercise 1 — annotate these. Write the full signature line with hints for: (a) counts errors in a list of lines, returns a number (b) takes a test name and seed, returns a job id string (c) takes a list of statuses, returns the count and pass count (d) prints a summary, returns nothing.

Exercise 2 — prove they are inert. Write a function annotated -> int that returns a list, call it, and print the result and its type. Explain in one sentence what the annotation accomplished.

Exercise 3 — read them back. Print __annotations__ for a three-parameter annotated function. Say what the dictionary's keys are and which one is not a parameter.

Exercise 4 — collections. Give the annotation for: (a) a list of integers (b) a dictionary of test name to error count (c) a set of failing test names (d) a tuple of a name and a runtime.

Exercise 5 — the None return. Annotate a function that prints and returns nothing, then say what the annotation tells a caller that the body alone does not.

Exercise 6 — hint or docstring. For each fact, say whether it belongs in a type hint, a docstring, or both: (a) the argument is a list of strings (b) the function empties its argument (c) the result is a count (d) an unknown severity is reported as "unknown".

Exercise 7 — the wrong hint. Write a function whose hint says -> str and which returns an integer under one condition. Run both paths. Explain why this is worse than leaving the hint off.

Exercise 8 — design pressure. Find a function in your Module 11 work whose parameter you cannot give a single type. Say what two jobs it is doing, and how you would split it.

12. Summary

  • name: type annotates a parameter; -> type annotates the return.
  • Python does not check them. A function annotated -> int can return a string and nothing complains.
  • They are not conversions and not validation. If a value must be checked, you check it.
  • They are stored in __annotations__, a dictionary keyed by parameter name plus "return" — which is how external tools read them.
  • Built-in type names are the annotations: str, int, float, bool.
  • Collections say what they contain: list[str], dict[str, int], tuple[int, int], set[str].
  • An older capitalised spelling exists in pre-3.9 code and means the same thing; write the lowercase form.
  • -> None means do not use the result, which is Chapter 11.8's distinction written into the signature.
  • The value is editors, a separate checking tool, and design pressure — never run-time safety.
  • Leave them out when they would be a guess. A wrong hint misleads exactly like a wrong docstring.

The next chapter looks at a function that calls itself.

Where this fits

Part of the Python curriculum.