Аннотации функций: возвращаемые значения

Раздел: Стиль кода -> Аннотации функций

Основы аннотации возвращаемого типа

Как указать, что функция возвращает значение определённого типа?

Начиная с Python 3.5, в язык введена возможность аннотировать возвращаемый тип функции с помощью стрелки -> после списка параметров. Основной синтаксис выглядит так:

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

Python возвращаемый тип функции (возвращаемый тип функции в python)

Здесь -> int сообщает, что функция add должна вернуть целое число. Это не влияет на выполнение программы, но статические анализаторы (например, mypy) могут проверять корректность типов.

Типичные ошибки и проблемы:

  • Пропуск аннотации возвращаемого типа приводит к тому, что mypy считает его Any, что снижает строгость проверки.
  • Несоответствие фактического возвращаемого значения и аннотации вызывает ошибку mypy.
  • В Python 3.5–3.9 для сложных типов (например, список чисел) требуется импорт из модуля typing.

Как аннотировать функцию, которая может не возвращать значение (None)?

Для функций, которые завершаются без явного return или возвращают None, обычно указывают -> None.

def print_message(msg: str) -> None:
    print(msg)

Если функция иногда возвращает значение, а иногда None, используют Optional[Тип] или эквивалент Union[Тип, None]. Начиная с Python 3.10, можно писать Тип | None.

from typing import Optional, Union

def find_user(user_id: int) -> Optional[str]:
    # ... возвращает имя пользователя или None
    if user_id in database:
        return database[user_id]
    return None

# Эквивалентные записи:
# def find_user(user_id: int) -> Union[str, None]:
# def find_user(user_id: int) -> str | None:

Проблемы: Частая ошибка - использование Optional для типов, которые не могут быть None. Например, def func() -> Optional[int]: подразумевает, что может быть None, хотя функция всегда возвращает целое.

Как указать, что функция возвращает значение одного из нескольких типов (Union)?

Когда функция может возвращать значения разных типов, применяют Union.

from typing import Union

def parse_number(value: str) -> Union[int, float]:
    try:
        return int(value)
    except ValueError:
        return float(value)

С Python 3.10 используется синтаксис int | float.

Ошибки: При большом количестве альтернатив Union становится громоздким. В таких случаях лучше пересмотреть архитектуру или использовать Any, но это снижает безопасность типов.

Когда стоит использовать Any для возврата?

Если возвращаемое значение действительно может быть любого типа (например, десериализация JSON), допустимо написать -> Any. Однако это отключает проверку типов, поэтому применяется редко.

from typing import Any

def load_json(data: str) -> Any:
    import json
    return json.loads(data)

Злоупотребление Any лишает преимуществ статической типизации. Лучше максимально конкретизировать тип (например, dict[str, Any]).

Как аннотировать возврат функции, возвращающей другую функцию (Callable)?

Используется конструкция Callable[[аргументы], возвращаемый_тип].

from typing import Callable

def create_multiplier(factor: int) -> Callable[[float], float]:
    def multiply(x: float) -> float:
        return x * factor
    return multiply

Здесь Callable[[float], float] означает функция, принимающая float и возвращающая float.

Проблемы: сложные сигнатуры (например, с переменным числом параметров) трудно выразить через Callable. Для этого используют Protocol или TypedDict.

Как обобщить возвращаемый тип с помощью TypeVar (дженерики)?

Когда функция возвращает значение, тип которого зависит от входных данных, применяют TypeVar.

from typing import TypeVar, List

T = TypeVar('T')

def first_element(lst: List[T]) -> T:
    return lst[0]

Теперь first_element([1,2,3]) выведет int, а first_element(['a','b']) - str.

Ошибки: неправильная привязка TypeVar (например, без указания ограничений) может привести к неопределённому поведению. Также для нескольких разных типов входных данных нужно определять отдельные TypeVar.

Как задать разные возвращаемые типы для разных сценариев вызова (overload)?

Декоратор @typing.overload позволяет определить несколько сигнатур для одной функции. Полезно, когда тип возвращаемого значения зависит от типа аргумента или его присутствия.

from typing import overload, Union

@overload
def to_str(value: int) -> str: ...
@overload
def to_str(value: float) -> str: ...
@overload
def to_str(value: None) -> str: ...

def to_str(value: Union[int, float, None]) -> str:
    if value is None:
        return ""
    return str(value)

Реализация должна покрывать все перегруженные варианты. mypy использует перегрузки для точного вывода типов.

Ошибки: забытая реализация или несоответствие между перегрузками и телом функции. Также перегрузки не проверяются во время выполнения, только статически.

Как ограничить возвращаемое значение конкретными литералами (Literal)?

Если функция возвращает только фиксированный набор строк или чисел, применяют Literal.

from typing import Literal

def get_status() -> Literal['ok', 'error', 'pending']:
    # ...
    return 'ok'

Теперь mypy проверит, что возвращается только одно из этих значений.

Проблемы: Literal требует точного совпадения. Нельзя вернуть переменную, содержащую такую строку, если она не объявлена как Literal.

Как определить собственный возвращаемый тип с помощью Protocol?

Protocol позволяет определить структурный тип (утиная типизация) для возвращаемого объекта. Полезно, если нужно описать интерфейс без наследования.

from typing import Protocol

class Named(Protocol):
    name: str
    def greet(self) -> str: ...

def create_person(name: str) -> Named:
    class Person:
        def __init__(self, name: str):
            self.name = name
        def greet(self) -> str:
            return f"Hello, {self.name}"
    return Person(name)

Функция гарантирует, что возвращаемый объект имеет атрибут name и метод greet.

Ошибки: непроверяемые атрибуты, если они не реализованы в возвращаемом объекте, mypy выдаст ошибку. Также Protocol не может быть инстанцирован напрямую.

Расширенные примеры использования возвращаемых типов

Ниже приведены примеры, иллюстрирующие одновременное использование нескольких механизмов типизации.

Пример 1. Перегрузка с дженериками для функции объединения списков

Функция, которая принимает список и возвращает его первый элемент, но если список пустой – возвращает None. Используем overload и TypeVar для точного вывода типа.

Пример
from typing import TypeVar, overload, Sequence, Optional

T = TypeVar('T')

@overload
def head(lst: Sequence[T]) -> T: ...
@overload
def head(lst: Sequence[T]) -> None: ...
# На самом деле корректно: если список пуст, вернется None, иначе T.
# Но mypy требует неоднозначности, поэтому используем Union
# Лучше:
# @overload
# def head(lst: Sequence[T]) -> T: ...
# @overload
# def head(lst: Sequence[T]) -> None: ...
# Реализация:
def head(lst: Sequence[T]) -> Optional[T]:
    return lst[0] if lst else None

Проверка mypy для вызовов:

Пример
# вызовы:
result1 = head([1, 2, 3])      # ожидается int (из первой перегрузки)
result2 = head([])              # ожидается None (из второй перегрузки)
reveal_type(result1)  # mypy: int
reveal_type(result2)  # mypy: None
# Результат mypy (при запуске с --strict):
# main.py:19: note: Revealed type is "builtins.int"
# main.py:20: note: Revealed type is "None"

Объяснение: Благодаря overload mypy выбирает правильную сигнатуру в зависимости от аргумента. Если список содержит элементы, возвращается T; если пуст – None. В реализации используется Optional[T].

Пример 2. Использование Protocol для возврата конфигурации

Создадим протокол для объекта конфигурации с полями server и port. Функция load_config возвращает объект, соответствующий этому протоколу.

Пример
from typing import Protocol, Optional

class Config(Protocol):
    server: str
    port: int

def load_config(file_path: str) -> Optional[Config]:
    # Имитация загрузки из файла
    if file_path.endswith('.json'):
        # возвращаем dict, который соответствует протоколу (утиная типизация)
        return {'server': 'localhost', 'port': 8080}
    else:
        return None

config = load_config('settings.json')
if config:
    print(config.server, config.port)
# Результат выполнения: localhost 8080

Объяснение: Protocol задаёт интерфейс. Возвращаемый словарь имеет атрибуты server и port, поэтому mypy считает его удовлетворяющим протоколу. При возврате None используется Optional.

Пример 3. Рекурсивный тип для древовидной структуры

Определим тип для узла дерева, который может содержать вложенные списки узлов. Используем самоссылающийся тип.

Пример
from typing import List, Union, TypeAlias

# В Python 3.12+ можно использовать TypeAlias
TreeNode: TypeAlias = Union[int, List['TreeNode']]

def make_tree(data: list) -> TreeNode:
    # упрощённая реализация
    if not data:
        return 0
    return [make_tree(x) for x in data]

Такой тип сложно аннотировать полностью, поэтому часто прибегают к Any или object, но при необходимости указывают Union с рекурсивной ссылкой.

Проблема: mypy может не справиться с глубокой рекурсией или циклическими зависимостями. В таких случаях используют отложенную аннотацию (строка с именем типа).

Пример 4. Использование TypeVar с ограничением

Ограничим TypeVar только числовыми типами (int, float).

Пример
from typing import TypeVar, Union

Number = TypeVar('Number', int, float)

def add_numbers(a: Number, b: Number) -> Number:
    return a + b

result = add_numbers(1, 2)    # OK
result2 = add_numbers(1.0, 2) # OK
# result3 = add_numbers(1, '2')  # mypy ошибка: str не подходит

Объяснение: TypeVar с ограничениями гарантирует, что оба аргумента и возврат будут одного из указанных типов. Это строже, чем Union.

Возвращаемый тип функции в Python - comments

En
Python возвращаемый тип функции (python)