Python: Wie kopiert man eine Datei?

shutil bietet dafür vier Funktionen, und sie unterscheiden sich darin, was sie mitnehmen. Seit Python 3.14 gibt es zusätzlich eine Methode in pathlib — die ist neuer als jede Antwort, die man dazu findet.

Zuerst die neue

from pathlib import Path

Path("quelle.txt").copy("ziel.txt")

Path.copy() kam mit Python 3.14, zusammen mit Path.copy_into() und Path.move(). Davor konnte pathlib mit Pfaden fast alles außer kopieren — deshalb greift jede ältere Antwort zu shutil.

Beachte die Voreinstellung:

src.copy(target)             : -rw-r--r--  mtime=<now>
copy(preserve_metadata=True) : -rwxr-x---  mtime=<original>

Path.copy() kopiert nur den Inhalt, solange du nicht mehr verlangst. Es verhält sich also wie shutil.copyfile und nicht wie copy2.

Und um in ein Verzeichnis statt auf einen Namen zu kopieren:

src.copy_into(a_directory) : created True

Wenn du auf 3.13 oder älter bist — was realistischerweise die meisten Projekte sind — lies weiter.

Die vier shutil-Funktionen

Der Unterschied liegt darin, was sie erhalten. Die Quelle ist hier -rwxr-x--- mit einer mtime von gestern:

source    : -rwxr-x---  mtime=1788078255
copyfile  -> -rw-r--r--  mtime=1788164655
copy      -> -rwxr-x---  mtime=1788164655
copy2     -> -rwxr-x---  mtime=1788078255
  • copyfile — der Inhalt. Das Ziel bekommt frische Rechte aus deiner umask.
  • copy — Inhalt und Zugriffsrechte.
  • copy2 — Inhalt, Rechte und Zeitstempel.

copy2 kommt cp -p am nächsten und ist die Wahl, wenn die Kopie das Original vertreten soll — eine Sicherung, ein vorbereitetes Deployment, alles wo später ein Werkzeug auf die mtime schaut.

Der Unterschied, bei dem es nicht um Metadaten geht

Dieser erwischt einen häufiger:

shutil.copy(src, a_directory)     : created True
shutil.copyfile(src, a_directory) -> IsADirectoryError: [Errno 21] Is a directory

copy und copy2 nehmen ein Verzeichnis als Ziel entgegen und behalten den ursprünglichen Dateinamen. copyfile verlangt einen vollständigen Zielpfad.

shutil.copy(f, "/backup/") funktioniert also und shutil.copyfile(f, "/backup/") nicht — unabhängig davon, was du erhalten wolltest.

Eine Datei auf sich selbst kopieren

copyfile(src, src) -> SameFileError: ... are the same file

SameFileError ist eine Unterklasse von OSError, du kannst also gezielt diesen Fall abfangen. Das ist wissenswert, wenn du Quelle und Ziel getrennt zusammenbaust und sie zusammenfallen könnten — ohne die Prüfung würdest du die Datei abschneiden, bevor du sie liest.

Was copy2 trotzdem nicht kopiert

copy2 verwendet copystat, das Modus, Zeitstempel und, wo unterstützt, Flags und erweiterte Attribute abdeckt. Den Eigentümer deckt es nicht ab.

Kopierst du eine Datei, die jemand anderem gehört, gehört dir die Kopie. Das zu ändern braucht os.chown und in der Praxis root. Wenn du als Dienstkonto Dateien umherschiebst und der Eigentümer eine Rolle spielt, ist das ein Schritt, den du selbst schreiben musst.

Ganze Verzeichnisse

copytree : True
again -> FileExistsError
with dirs_exist_ok=True : fine (added in 3.8)

copytree verweigert ein bereits vorhandenes Ziel. dirs_exist_ok=True fügt hinein. Es nimmt außerdem ein ignore= entgegen, wofür shutil.ignore_patterns den üblichen Fall abdeckt:

shutil.copytree(quelle, ziel, ignore=shutil.ignore_patterns("*.pyc", "__pycache__"))

Wenn du Dateiobjekte statt Pfade hast

with open(quelle, "rb") as fsrc, open(ziel, "wb") as fdst:
    shutil.copyfileobj(fsrc, fdst)
copyfileobj : 'hello\n'

copyfileobj arbeitet mit allem Dateiähnlichen und ist damit die Wahl für den Body einer HTTP-Antwort, einen Upload-Strom oder ein Element aus einem tar- oder zip-Archiv. Es kopiert in Blöcken, die Dateigröße wird also nicht zu deinem Speicherverbrauch.

Was man nicht tun sollte

Der Einzeiler, der in älterem Code auftaucht:

open(ziel, "w").write(open(quelle).read())

Drei Probleme. Er liest die ganze Datei in den Speicher. Er lässt beide Dateihandles offen. Und im Textmodus dekodiert und kodiert er neu, was Zeilenenden umschreibt und alles zerlegt, was kein Text ist:

reading a binary file as text -> UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc3 ...

Hier hat es geworfen, und das ist der gute Ausgang. Bei einer Datei, die sich zufällig sauber dekodieren lässt, wäre es durchgelaufen und hätte dir stillschweigend eine andere Datei hinterlassen.

Ein Grund, shutil dem Selbstbau vorzuziehen

8 MiB, shutil.copyfile   : 1.8 ms
8 MiB, manual 64K chunks : 2.1 ms

Seit Python 3.8 verwendet shutil die Zero-Copy-Aufrufe der jeweiligen Plattform, wo es sie gibt — os.sendfile und copy_file_range unter Linux, fcopyfile unter macOS. Die Daten müssen dabei gar nicht durch deinen Prozess.

Der Abstand oben ist klein, weil die Datei im Page Cache lag und das Container-Dateisystem ein Overlay ist. Der Punkt ist nicht, dass shutil dramatisch schneller wäre — sondern dass eine selbst geschriebene Blockschleife nicht schneller sein kann und mehr Möglichkeiten hat, falsch zu sein.

Hinweis zu Netcup (Werbung)

Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).