Há exatamente um motivo pelo qual o seguinte é preferido:
with open('filename.txt') as fp:
for line in fp:
print line
Todos nós somos prejudicados pelo esquema relativamente determinístico de contagem de referência do CPython para coleta de lixo. Outras implementações hipotéticas do Python não necessariamente fecham o arquivo "com rapidez suficiente" sem owith bloco se eles usarem algum outro esquema para recuperar a memória.
Em tal implementação, você pode receber um erro "muitos arquivos abertos" no sistema operacional se o seu código abrir arquivos mais rapidamente do que o coletor de lixo chama finalizadores em identificadores de arquivos órfãos. A solução usual é acionar o GC imediatamente, mas esse é um truque desagradável e deve ser feito por todos os funções que possam encontrar o erro, incluindo as das bibliotecas. Que pesadelo.
Ou você pode simplesmente usar o withbloco.
Pergunta bônus
(Pare de ler agora se estiver interessado apenas nos aspectos objetivos da pergunta.)
Por que isso não está incluído no protocolo do iterador para objetos de arquivo?
Esta é uma pergunta subjetiva sobre o design da API, por isso tenho uma resposta subjetiva em duas partes.
No nível interno, isso parece errado, porque faz com que o protocolo do iterador faça duas coisas separadas - iterar sobre linhas e feche o identificador do arquivo - e geralmente é uma má idéia fazer uma função de aparência simples executar duas ações. Nesse caso, parece especialmente ruim porque os iteradores se relacionam de maneira quase funcional e baseada em valores com o conteúdo de um arquivo, mas o gerenciamento de identificadores de arquivo é uma tarefa completamente separada. Esmagar os dois, invisivelmente, em uma ação, é surpreendente para os humanos que lêem o código e torna mais difícil raciocinar sobre o comportamento do programa.
Outras línguas chegaram essencialmente à mesma conclusão. Haskell flertou brevemente com o chamado "IO preguiçoso", que permite iterar sobre um arquivo e fechá-lo automaticamente quando você chegar ao final do fluxo, mas é quase universalmente desencorajado o uso de IO preguiçoso em Haskell atualmente e Haskell os usuários passaram para o gerenciamento de recursos mais explícito, como o Conduit, que se comporta mais como o withbloco do Python.
Em um nível técnico, há algumas coisas que você pode querer fazer com um identificador de arquivo no Python que não funcionaria tão bem se a iteração fechasse o identificador de arquivo. Por exemplo, suponha que eu precise repetir o arquivo duas vezes:
with open('filename.txt') as fp:
for line in fp:
...
fp.seek(0)
for line in fp:
...
Embora este seja um caso de uso menos comum, considere o fato de que talvez eu tenha adicionado as três linhas de código na parte inferior a uma base de código existente que originalmente possuía as três principais linhas. Se a iteração fechou o arquivo, eu não seria capaz de fazer isso. Portanto, manter a iteração e o gerenciamento de recursos separados facilita a composição de trechos de código em um programa Python maior e funcional.
A composição é um dos recursos de usabilidade mais importantes de um idioma ou API.