Skip to content
This page has been auto-translated and may contain errors.View in English

Funções ​

Conforme seus programas crescem, você vai escrever a mesma lógica em mais de um lugar. Funções permitem que você escreva a lógica uma vez, dê um nome a ela e a use em qualquer lugar. Corrija em um só lugar e toda chamada recebe a correção automaticamente.

Funções são a principal unidade de reuso de código e abstração. Elas encapsulam um comportamento, dão a ele um nome, definem uma interface clara (seus parâmetros e valor de retorno) e o tornam chamável de qualquer lugar. Funções bem nomeadas também funcionam como documentação: validate_email() te diz o que um bloco de código faz sem que você precise ler o corpo dele.

Em Python, uma função é um objeto de primeira classe: um valor que você pode manipular como qualquer outro, então ela pode ser atribuída a uma variável, armazenada em uma lista ou dict, passada para outra função como argumento e retornada de dentro de uma função. O def cria esse objeto e o vincula a um nome no escopo atual (a região do código onde esse nome é visível). Tratar funções como valores é o que torna possíveis padrões como sorted(key=...) e pequenas tabelas de plugins, e vale a pena se acostumar com isso desde já.

python
def greet(name):
    return f"Hello, {name}!"

print(greet("Alice"))   # "Hello, Alice!"
print(greet("Bob"))     # "Hello, Bob!"

Escreva uma vez, use em qualquer lugar, corrija em um só ponto.

Definindo uma função ​

A palavra-chave def inicia uma definição de função, seguida do nome, parênteses, dois-pontos e um corpo indentado. Uma função não faz nada até que você a chame. Defina-a com def, depois a chame pelo nome com ().

def é uma instrução que cria um objeto de função e o vincula ao nome dado no escopo atual. O corpo não é executado no momento da definição; ele só é executado quando a função é chamada. Funções sem uma instrução return retornam implicitamente None.

def constrói um objeto de função e o vincula ao nome no escopo atual. O corpo não é executado quando o Python lê o def, ele só roda quando você chama a função. Essa separação importa na prática: um erro de digitação dentro do corpo de uma função (um nome que não existe, por exemplo) fica em silêncio até que a função seja de fato chamada, então uma função que você nunca chama pode escconder um NameError que só aparece em produção no dia em que alguém a chamar.

python
def say_hello():
    print("Hello!")

say_hello()   # chama a função
JunoDefinindo uma função Escreva def nome(): e indente o corpo abaixo. Nada acontece até você chamá-la com nome(), o def só faz a preparação. E se você esquecer o return, a função devolve None silenciosamente, o que já me pegou mais de uma vez no começo.
JunoDefinindo uma funçãodef cria a função e a vincula a um nome, mas o corpo só roda quando você a chama. Sem return, a função retorna None. Tenha isso em mente quando um resultado voltar vazio sem motivo aparente.
JunoDefinindo uma função O corpo não roda no momento do def, só quando você chama a função, então um nome quebrado ali dentro pode passar despercebido até que algo de fato a chame. Uma função sem return retorna None, o que é um bug silencioso à sua própria maneira quando quem chama espera um valor.

Parâmetros e argumentos ​

Parâmetros são as entradas que sua função espera. Liste-os dentro dos parênteses. Quando você chama a função, os valores que você passa são correspondidos aos parâmetros na ordem.

Parâmetros definem a interface de uma função. Argumentos são os valores concretos passados no momento da chamada. Argumentos posicionais são correspondidos por posição; argumentos nomeados (keyword) são correspondidos por nome. Valores padrão tornam os parâmetros opcionais.

No momento da chamada, argumentos posicionais se vinculam da esquerda para a direita e argumentos nomeados se vinculam pelo nome. Passar o mesmo parâmetro das duas formas, ou de um jeito que o Python não consegue posicionar, gera TypeError na chamada, não dentro do corpo. O Python também permite restringir uma assinatura: um * isolado torna tudo depois dele apenas nomeado (quem chama precisa nomear), e um / torna tudo antes dele apenas posicional. Use argumentos apenas nomeados para flags booleanas e assinaturas com muitos parâmetros, onde um True posicional no lugar errado é o tipo de erro que compila sem problema e vai para produção com um bug.

python
def greet(name, greeting):
    print(f"{greeting}, {name}!")

greet("Alice", "Hello")    # "Hello, Alice!"
greet("Bob", "Hi")         # "Hi, Bob!"

Parâmetro é o nome na definição da função. Argumento é o valor real que você passa quando a chama. Na prática, as pessoas usam os termos de forma intercambiável; a distinção importa principalmente ao ler documentação.

JunoParâmetros e argumentos Parâmetros são os nomes que você lista nos parênteses, argumentos são os valores reais que você entrega na chamada. O Python os alinha da esquerda para a direita, então a ordem em que você os passa é a ordem em que eles caem.
JunoParâmetros e argumentos Parâmetros são os nomes na definição, argumentos são os valores na chamada. Os posicionais se vinculam da esquerda para a direita, então a posição é o contrato. Os dois termos acabam sendo usados de forma intercambiável, a diferença só importa quando você lê documentação.
JunoParâmetros e argumentos Argumentos posicionais se vinculam da esquerda para a direita, os nomeados pelo nome, e um conflito gera TypeError direto na chamada. Um * isolado obriga quem chama a nomear o que vem depois, o que é a solução barata para uma função cujas flags True/False seriam, do contrário, um jogo de adivinhação no local da chamada.

Valores padrão ​

Você pode dar a um parâmetro um valor padrão. Se quem chama não fornecer esse argumento, o padrão é usado. Parâmetros com valores padrão devem vir depois dos parâmetros sem valores padrão.

Valores padrão tornam os parâmetros opcionais. Eles são avaliados uma única vez, no momento da definição, não em cada chamada. Isso importa para padrões mutáveis: def f(items=[]) compartilha a mesma lista entre todas as chamadas. A solução é usar None como padrão e criar a lista dentro do corpo da função.

O detalhe que costuma pegar: um valor padrão é construído uma única vez, quando o def é executado, e compartilhado por toda chamada que o usa. Para um padrão imutável como 0 ou "Hello" (um que você não pode alterar no lugar), isso não é problema. Para um padrão mutável como uma lista ou dict (um que você pode alterar no lugar), é uma armadilha, porque toda chamada que recorre ao padrão reutiliza o mesmo objeto:

python
def add_item(item, items=[]):   # a lista é criada uma vez, compartilhada para sempre
    items.append(item)
    return items

add_item("a")   # ['a']
add_item("b")   # ['a', 'b']  <- não é uma lista nova

A solução é a padrão: use None como valor padrão e construa um contêiner novo dentro do corpo.

python
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items
python
def greet(name, greeting="Hello"):
    print(f"{greeting}, {name}!")

greet("Alice")           # "Hello, Alice!"
greet("Alice", "Hi")     # "Hi, Alice!"

Parâmetros com valores padrão devem vir depois dos parâmetros sem valores padrão.

JunoValores padrão Dê um valor padrão a um parâmetro e ele se torna opcional, quem chama pode pular ele. Os que têm padrão precisam vir depois dos obrigatórios, essa ordem é uma regra que o Python impõe. Poupa você de escrever o mesmo valor em toda chamada.
JunoValores padrão Padrões tornam um parâmetro opcional e precisam vir depois dos obrigatórios. A pegadinha: um padrão é avaliado uma única vez na definição, então nunca use um mutável como []. Use None como padrão e construa a lista dentro do corpo.
JunoValores padrão Um padrão é construído uma vez no momento do def e compartilhado entre as chamadas, então items=[] acumula silenciosamente de chamada em chamada. Use None como padrão e crie um novo dentro do corpo. Esse é um dos deslizes de Python que mais aparece em code review de verdade.

Argumentos nomeados ​

Ao chamar uma função, você pode nomear os argumentos. Isso torna as chamadas mais legíveis, especialmente para funções com muitos parâmetros, e permite passá-los em qualquer ordem.

Argumentos nomeados tornam as chamadas de função autoexplicativas. Você pode misturar posicionais e nomeados: os posicionais devem vir primeiro. Para funções com flags booleanas ou muitos parâmetros de tipos parecidos, argumentos nomeados evitam erros silenciosos causados por passar argumentos na ordem errada.

Argumentos nomeados se vinculam pelo nome, então ficam legíveis e livram você de precisar lembrar posições. A regra na chamada: argumentos posicionais vêm primeiro, os nomeados depois, e nomear o mesmo parâmetro duas vezes gera TypeError. A jogada de design que vale a pena fazer é o inverso: coloque um * isolado na sua própria assinatura para obrigar quem chama a nomear um parâmetro (def connect(host, *, timeout=30)). Isso não custa nada e transforma um connect("db", 30) opaco em um connect("db", timeout=30) que continua legível mesmo depois que alguém adicionar um parâmetro no meio.

python
def describe_player(name, score, level):
    print(f"{name} | Score: {score} | Level: {level}")

describe_player("Alice", 87, 5)                        # posicional
describe_player(name="Alice", level=5, score=87)       # nomeado, em qualquer ordem
describe_player("Alice", level=5, score=87)            # misto: posicional primeiro
JunoArgumentos nomeados Nomeie os argumentos na chamada (score=87) e a chamada se explica sozinha, além de permitir passá-los em qualquer ordem. A única regra: qualquer argumento posicional vem antes dos nomeados. Ótimo para funções com uma pilha de parâmetros.
JunoArgumentos nomeados Nomear os argumentos torna uma chamada autoexplicativa e independente da ordem, com os posicionais exigidos primeiro. Para assinaturas extensas ou flags booleanas, prefira argumentos nomeados para que ninguém precise contar posições para ler a chamada.
JunoArgumentos nomeados Argumentos nomeados se vinculam pelo nome e precisam vir depois de qualquer posicional. A verdadeira vantagem está na sua própria assinatura: um * isolado obriga quem chama a nomear o que vem depois, então a chamada continua legível mesmo depois que alguém encaixar um novo parâmetro no meio.

Valores de retorno ​

return envia um valor de volta para quem chamou a função. Sem return, uma função devolve None. Assim que o return é executado, a função é encerrada imediatamente. Qualquer código depois dele naquele bloco é ignorado.

return encerra a função e passa um valor para quem a chamou. Uma função sem um return explícito retorna implicitamente None. return pode aparecer em qualquer lugar do corpo da função e pode ser usado mais de uma vez; o primeiro que for alcançado encerra a função. Isso torna os retornos antecipados úteis para cláusulas de guarda.

return entrega um valor para quem chamou e encerra a função ali mesmo, e uma função que chega ao fim sem um return retorna None. Múltiplos retornos são um bom estilo, não um problema: um return antecipado para o caso de falha ou de borda no início da função (o padrão de "cláusula de guarda") fica muito mais legível do que aninhar o corpo inteiro dentro de um único if gigante. Um detalhe sutil que é bom conhecer antes que ele te surpreenda: um return dentro de um bloco try ainda executa o bloco finally correspondente antes de retornar, então a limpeza no finally acontece mesmo quando você retorna antecipadamente.

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

result = add(3, 4)   # result = 7
print(result)

return também encerra a função imediatamente. Qualquer código depois dele naquele bloco não é executado.

JunoValores de retornoreturn devolve um valor para quem chamou a função, e a função para ali mesmo, qualquer coisa depois dele é ignorada. Se você não colocar return, você recebe None de volta. print mostra um valor, return devolve ele para uso, demorei um tempo para sentir essa diferença.
JunoValores de retornoreturn passa um valor de volta e encerra imediatamente, sem return significa None. Você pode ter vários, e um return antecipado para o caso de borda no início geralmente fica mais limpo do que envolver tudo em um único if gigante.
JunoValores de retornoreturn sai ali mesmo, e chegar ao fim sem retornar resulta em None. Aposte em retornos antecipados como cláusulas de guarda em vez de aninhamento profundo. E lembre que um return dentro de try ainda executa o bloco finally, então a limpeza ali dispara até em uma saída antecipada.

Retornando múltiplos valores ​

O Python permite retornar múltiplos valores separando-os com vírgulas. Quem chama a função os recebe como uma tupla e pode desempacotá-los em nomes separados em uma única linha.

Retornar múltiplos valores com uma vírgula os empacota em uma tupla. Quem chama desempacota com nomes correspondentes. Isso é idiomático em Python para funções que naturalmente produzem mais de um resultado. Não é um recurso especial; é empacotamento e desempacotamento de tupla.

return a, b empacota os valores em uma tupla, e quem chama os separa com low, high = f(). Isso fica legível para dois ou três resultados. Além disso, a posição se torna um risco: ninguém que chama a função lembra se o terceiro item era a contagem ou a média, e uma dupla trocada é um bug silencioso. Nesse ponto, retorne algo nomeado em vez disso, um NamedTuple ou um @dataclass, para que quem chama use result.average em vez de result[2]. Anote o retorno como tuple[int, str] de qualquer forma, para que um verificador de tipos consiga detectar quem chama desempacotando o número errado de valores.

python
def min_max(numbers):
    return min(numbers), max(numbers)

low, high = min_max([3, 7, 1, 9, 4])
print(low, high)   # 1 9

A sintaxe low, high = ... é desempacotamento: o Python atribui cada valor retornado ao nome correspondente.

JunoRetornando múltiplos valores Separe valores com uma vírgula e a função os retorna juntos como uma tupla. Quem chama captura eles em uma linha, low, high = min_max(...), um nome por valor. Útil sempre que uma função naturalmente produz mais de uma resposta.
JunoRetornando múltiplos valores Uma vírgula empacota os valores em uma tupla, quem chama desempacota com nomes correspondentes. É simplesmente empacotamento e desempacotamento de tupla, não um recurso especial. Fica limpo para dois ou três resultados, menos além disso.
JunoRetornando múltiplos valores Retorno com vírgula empacota uma tupla, ótimo para dois ou três valores. Além disso, a posição se torna uma armadilha que ninguém consegue ler, então retorne um NamedTuple ou @dataclass e deixe quem chama usar result.average em vez de contar índices.

Escopo ​

Variáveis criadas dentro de uma função existem apenas dentro daquela função. Você não pode vê-las de fora. Variáveis definidas fora de todas as funções são visíveis em todo lugar, mas você não pode alterá-las de dentro de uma função sem uma declaração explícita.

Um nome criado dentro de uma função é local: existe só ali e desaparece quando a função retorna. Um nome definido no nível superior do arquivo é global. Ler uma variável global de dentro de uma função funciona sem cerimônia, mas atribuir um valor a ela precisa de global name, senão o Python trata a atribuição como a criação de uma variável local nova que oculta a global. Na prática, você quase nunca precisa de global: passar valores como argumentos e devolver resultados com return mantém os efeitos de uma função visíveis no local da chamada.

O Python resolve um nome com a regra LEGB, verificando quatro lugares em ordem: Local (esta função), Enclosing (uma função externa que envolve esta), Global (o módulo) e depois Built-in (nomes como len). A leitura percorre essa cadeia; a atribuição sempre cria uma variável local, a menos que você diga o contrário. Essa assimetria é a fonte do bug: count += 1 contra uma variável global sem uma linha global count gera UnboundLocalError, porque a atribuição marca count como local para a função inteira, e a leitura acontece antes de qualquer valor local existir. global name faz uma atribuição voltar a valer no nível do módulo; nonlocal name aponta para a função envolvente mais próxima, o que é como uma closure atualiza uma variável da função que a criou. Trate ambos como um sinal de alerta: uma função que se estende para fora e altera estado externo é mais difícil de testar do que uma que recebe entradas e retorna saídas.

JunoEscopo Uma variável criada dentro de uma função vive só ali, o de fora não consegue vê-la. Você pode ler uma variável externa de dentro, mas alterar uma precisa de uma linha com global antes. Use isso quase nunca: passe valores para dentro, retorne resultados para fora, e a função continua simples de acompanhar.
JunoEscopo Nomes criados dentro de uma função são locais e desaparecem quando ela retorna. Ler uma global é de graça, escrever nela precisa de global name, ou o Python cria uma local que a oculta. Prefira receber argumentos e devolver com return para que os efeitos de uma função continuem visíveis na chamada.
JunoEscopo LEGB para buscas, mas atribuição sempre cria uma local, a menos que você declare global ou nonlocal, e é por isso que count += 1 em uma global lança UnboundLocalError. As duas palavras-chave são um sinal de alerta: uma função que altera estado externo é mais difícil de testar do que uma que recebe entradas e retorna saídas.
python
def calculate():
    result = 42   # local a esta função
    return result

calculate()
print(result)   # NameError, result não existe aqui fora
python
count = 0

def increment():
    global count    # declara que você quer modificar a global
    count += 1

increment()
print(count)   # 1

Usar global deve ser o último recurso. Isso torna o código mais difícil de entender. Prefira passar valores como entrada e retorná-los como saída. Escopo se apoia diretamente em como a atribuição vincula um nome a um valor, assunto abordado no capítulo Variáveis e tipos.

*args e **kwargs ​

Às vezes você não sabe quantos argumentos uma função vai receber. *args coleta qualquer quantidade de argumentos posicionais em uma tupla. **kwargs coleta qualquer quantidade de argumentos nomeados em um dicionário. Os nomes args e kwargs são convenções; os asteriscos é que importam.

*args coleta o excesso de argumentos posicionais em uma tupla. **kwargs coleta o excesso de argumentos nomeados em um dict. Ambos podem ser combinados com parâmetros normais. Parâmetros normais vêm primeiro, depois *args, depois parâmetros apenas nomeados, depois **kwargs. Eles são úteis para funções wrapper que passam argumentos para outra função.

*args reúne os argumentos posicionais restantes em uma tuple, **kwargs reúne os argumentos nomeados restantes em um dict. O espelho disso acontece no local da chamada: func(*some_list) espalha uma lista em argumentos posicionais e func(**some_dict) espalha um dict em argumentos nomeados. Essa simetria é o que faz um wrapper de passagem funcionar, def timed(*args, **kwargs): return inner(*args, **kwargs) repassa o que recebeu sem nomear um único parâmetro. Útil, mas isso custa a assinatura legível: um wrapper que engole tudo em *args, **kwargs não mostra a quem chama nem ao verificador de tipos nada sobre o que ele aceita, então use isso quando você realmente precisar repassar argumentos arbitrários, não como o formato padrão para uma função comum.

python
def total(*args):
    return sum(args)

total(1, 2, 3)          # 6
total(1, 2, 3, 4, 5)   # 15
python
def display(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

display(name="Alice", score=87, level=5)

Você pode misturá-los com parâmetros normais. Os parâmetros normais vêm primeiro:

python
def describe(title, *tags, **metadata):
    print(f"{title} | tags: {tags} | meta: {metadata}")

describe("Python intro", "beginner", "python", author="Alice", year=2024)
Juno*args e **kwargs*args junta qualquer quantidade de argumentos posicionais extras como uma tupla, **kwargs junta os nomeados extras como um dict. As palavras args e kwargs são só convenção, o * e o ** é que fazem o trabalho de fato. Útil quando você não sabe de antemão quantas coisas vão chegar.
Juno*args e **kwargs*args coleta argumentos posicionais extras em uma tupla, **kwargs os nomeados extras em um dict. Ordem na assinatura: parâmetros normais, depois *args, depois apenas nomeados, depois **kwargs. Mais útil em wrappers que repassam argumentos direto para outra função.
Juno*args e **kwargs* e ** coletam em uma assinatura e espalham em uma chamada, o que é o que permite um wrapper repassar tudo com inner(*args, **kwargs). O custo é uma assinatura que não diz nada a quem chama, então reserve isso para repasses de verdade, não para funções comuns.

Docstrings ​

Uma docstring é uma string no topo de uma função que descreve o que ela faz. Editores e ferramentas do Python a usam para mostrar ajuda quando você passa o mouse sobre uma chamada de função. Use aspas triplas e escreva uma linha para funções simples.

Uma docstring é a primeira coisa no corpo da função, uma string em aspas triplas. Ferramentas a leem: help(func) a imprime, e seu editor a mostra quando você passa o mouse sobre uma chamada, então ela compensa justamente quando você já esqueceu o que a função faz. A convenção é uma linha de resumo, depois uma linha em branco e mais detalhes se necessário. Trate-a como obrigatória em qualquer coisa chamada a partir de mais de um lugar, opcional em um auxiliar descartável cujo nome já diz tudo.

Uma docstring é a primeira instrução no corpo de uma função, classe ou módulo, e deve dizer o que a função faz e o que ela retorna, não repetir a lista de parâmetros que o leitor já pode ver. Ela compensa em qualquer coisa cujo contrato não seja visível pela assinatura: os casos de borda, o que ela levanta como exceção, o que uma entrada vazia faz. Pule o bloco formal argumento por argumento quando os type hints na assinatura já trazem os tipos, esse detalhe pertence aos hints, não deve ser duplicado em prosa que fica desatualizada. O objetivo é a linha que você gostaria de ler às duas da manhã depurando uma chamada que você escreveu seis meses atrás.

python
def normalise(value, min_val, max_val):
    """Escala um valor para a faixa 0-1 dado o mínimo e o máximo conhecidos."""
    return (value - min_val) / (max_val - min_val)
python
def build_url(base, version, resource, *, secure=True):
    """
    Constrói a URL de um endpoint da API.

    Retorna uma string de URL totalmente qualificada. Se secure for False,
    a URL usará http em vez de https.
    """
    scheme = "https" if secure else "http"
    base = base.replace("https://", "").replace("http://", "")
    return f"{scheme}://{base}/{version}/{resource}"

Escreva uma docstring para qualquer função cujo propósito não seja claro a partir do nome e da assinatura.

JunoDocstrings Uma docstring é uma string em aspas triplas logo no topo de uma função, descrevendo o que ela faz. Seu editor a mostra quando você passa o mouse sobre a função depois, e é aí que você vai ficar feliz de ela estar ali. Escreva uma sempre que o nome e as entradas não deixarem o propósito claro por si só.
JunoDocstrings Primeira linha do corpo, entre aspas triplas: help() e seu editor mostram ela ao passar o mouse. Uma linha de resumo cobre a maioria das funções, adicione detalhes abaixo só quando o comportamento exigir. Obrigatória em algo chamado de vários lugares, dispensável em uma função de uma linha cujo nome já diz tudo.
JunoDocstrings Diga o que a função faz, o que ela retorna e os casos de borda, não uma lista de parâmetros repetida que o leitor já pode ver. Deixe os type hints carregarem os tipos para você não precisar manter os mesmos fatos em dois lugares. Busque a linha que você gostaria de ler às duas da manhã depurando uma chamada que escreveu meio ano atrás.

Type hints ​

Type hints permitem que você anote quais tipos uma função espera e retorna. O Python não os impõe em tempo de execução, mas editores os usam para identificar erros antes que você execute qualquer coisa. O -> antes dos dois-pontos especifica o tipo de retorno.

Type hints são documentação que ferramentas verificam. Editores e verificadores de tipos (mypy, pyright) os usam para identificar incompatibilidades de tipo antes da execução. Eles não têm efeito em tempo de execução no Python padrão. -> None é a anotação correta para funções sem valor de retorno. Para contêineres genéricos, use list[int], dict[str, int] (Python 3.9+).

Type hints não mudam nada em tempo de execução, o Python executa de forma idêntica com ou sem eles. Todo o valor deles está na camada externa ao programa: um verificador de tipos (mypy ou pyright) lendo o código antes dele rodar, e seu editor sinalizando uma chamada errada enquanto você digita. Então um hint que mente é pior do que nenhum hint, porque o verificador confia nele. Use list[int], dict[str, int] e str | None diretamente (sem precisar importar typing no Python moderno), e anote uma função que não retorna nada como -> None, o que também sinaliza que chamá-la dentro de uma expressão (x = log(...)) é um erro. O ganho aparece em funções chamadas de muitos lugares: o verificador detecta quem chama passando o tipo errado antes que isso apareça como uma falha confusa três níveis abaixo em tempo de execução.

python
def greet(name: str, score: int) -> str:
    return f"{name} scored {score}"
python
def log(message: str) -> None:
    print(f"[LOG] {message}")
python
def top_scores(scores: list[int], n: int) -> list[int]:
    return sorted(scores, reverse=True)[:n]

Type hints são opcionais, mas valiosos em qualquer função que será chamada de vários lugares. Eles são documentação que ferramentas podem verificar.

JunoType hints Type hints anotam o que uma função recebe e devolve, como name: str e -> str para o retorno. O Python não vai impor eles ao executar, mas seu editor os lê e te avisa antes de você rodar o código. Pense neles como anotações que as ferramentas conseguem checar.
JunoType hints Hints são documentação que um verificador de tipos pode confirmar, sem efeito em tempo de execução. -> None é a anotação correta quando uma função não retorna nada, e list[int] / dict[str, int] funcionam sem importar typing no Python moderno. Vale a pena em qualquer coisa chamada de vários lugares.
JunoType hints Hints não fazem nada em tempo de execução, o valor deles está no verificador e no editor que os leem, então um hint que mente é pior do que nenhum. Use str | None e list[int] diretamente, -> None para nenhum retorno. O ganho é detectar quem chama com o tipo errado antes que isso falhe três níveis abaixo.

Funções como valores ​

Funções em Python são valores, como strings ou números. Você pode atribuí-las a variáveis e passá-las para outras funções. É assim que sorted() aceita uma função em key=.

Funções são objetos de primeira classe: têm um tipo (function), podem ser armazenadas em variáveis e coleções, e podem ser passadas como argumentos ou retornadas como valores. Essa é a base de funções de ordem superior como sorted(key=...), map() e filter().

Uma função é um objeto que você passa por referência, nunca uma cópia, então entregar uma a outra função é barato. O padrão que surge disso é a closure: uma função definida dentro de outra que ainda alcança as variáveis da função externa depois que a função externa retornou. É assim que você constrói uma função configurada em tempo real, um make_multiplier(3) que retorna uma função que multiplica por 3, com o 3 capturado e retido. Dois cuidados no código real: uma closure captura a variável, não o valor dela no momento da captura, o que pega as pessoas que constroem closures dentro de um laço (todas veem o valor final do laço); e empilhar muitas funções retornadas torna os caminhos de chamada difíceis de rastrear, então use closures onde elas ficam claras e recorra a uma classe quando o estado crescer.

python
def double(x):
    return x * 2

def apply(func, value):
    return func(value)

apply(double, 5)   # 10

Passar funções como argumentos aparece o tempo todo com sorted(), map() e filter(). Você também vai ver isso no capítulo Lambdas e comprehensions.

JunoFunções como valores Uma função é um valor, como uma string ou um número, então você pode armazená-la em uma variável e entregá-la para outra função. É exatamente isso que permite passar uma função como key= para sorted(). Achei estranho no começo, depois fez sentido e passou a aparecer em todo lugar.
JunoFunções como valores Funções são objetos de primeira classe: você atribui, armazena em uma lista, passa, retorna. Esse é o motor por trás de sorted(key=...), map() e filter(), e de todo callback que você vai montar mais adiante.
JunoFunções como valores Passar uma função é uma referência, não uma cópia, e uma closure permite que uma função interna continue alcançando as variáveis da externa depois que ela retornou. Ela captura a variável, não o valor, então construir closures dentro de um laço causa problema toda vez. Quando o estado capturado cresce, uma classe fica mais legível.

Na prática ​

Duas funções que trabalham juntas: letter_grade converte uma nota em uma letra, e summarise a chama para cada nota em uma lista:

python
def letter_grade(score: int) -> str:
    if score >= 90:
        return "A"
    elif score >= 80:
        return "B"
    elif score >= 70:
        return "C"
    else:
        return "F"

def summarise(scores: list[int]) -> None:
    total = sum(scores)
    avg = total / len(scores)
    grades = []
    for s in scores:
        grades.append(letter_grade(s))
    print(f"Average: {avg:.1f}")
    print(f"Grades: {', '.join(grades)}")

summarise([87, 92, 74, 65, 91])

Um formatador de log e um processador de arquivo que o usa, com um parâmetro padrão dry_run que evita efeitos colaterais a menos que seja explicitamente desativado:

python
def format_log(level: str, message: str) -> str:
    return f"[{level.upper():5}] {message}"

def process_file(path: str, dry_run: bool = True) -> bool:
    print(format_log("info", f"Processing {path}"))
    if dry_run:
        print(format_log("info", "Dry run, no changes made"))
        return True
    return True

process_file("report.csv")
process_file("report.csv", dry_run=False)

Um normalizador de valor único e um normalizador de coluna construído sobre ele, com type hints e uma docstring. A função de coluna calcula a faixa uma única vez e reutiliza a função escalar para cada item:

python
def normalise(value: float, min_val: float, max_val: float) -> float:
    """Escala value para a faixa 0-1 dados o mínimo e o máximo conhecidos."""
    if max_val == min_val:
        return 0.0
    return (value - min_val) / (max_val - min_val)

def normalise_column(values: list[float]) -> list[float]:
    """Normaliza uma coluna inteira de valores."""
    lo, hi = min(values), max(values)
    return [normalise(v, lo, hi) for v in values]

raw = [10.0, 25.0, 5.0, 40.0, 15.0]
print(normalise_column(raw))

Os type hints aqui têm dois propósitos: documentam o que a função espera e permitem que um verificador de tipos identifique quem chama passando uma lista de strings por engano.