Make_archive Puts Files in the Wrong Place: Separate Output Path and Member Root
By ON Lab · Sources checked November 8, 2026
In shutil.make_archive, base_name chooses where the archive file is written. root_dir and base_dir describe the input tree and the names stored inside it. Changing root_dir is not a request to move the completed ZIP there. Check the returned output path and the complete member list separately.
Make the two namespaces visible
A historical CPython question expected root_dir to control the archive’s output location. Its discussion led to clearer documentation. Our Python 3.13.18 fixture used a separate input tree and output directory:
demo/
input/
project/
readme.txt
.internal.txt
assets/
note.txt
output/Use a fresh disposable demo directory for this example: the calls can replace an existing archive at the same output name. All fixture contents were synthetic. The hidden-looking .internal.txt file deliberately checks whether a name is excluded merely because it begins with a dot.
Keep a project prefix, or archive only its contents
from pathlib import Path
import shutil
import zipfile
project = Path("demo/input/project").resolve()
output = Path("demo/output").resolve()
output.mkdir(parents=True, exist_ok=True)
prefixed = shutil.make_archive(str(output / "prefixed"), "zip",
root_dir=project.parent, base_dir="project")
flat = shutil.make_archive(str(output / "flat"), "zip",
root_dir=project, base_dir=".")
for filename in [prefixed, flat]:
with zipfile.ZipFile(filename) as archive:
print(Path(filename).name, sorted(archive.namelist()))Both archives were written in output. Their stored names differed. The first kept a project/ prefix; the second stored the contents relative to the project root. In both cases, .internal.txt was included within the selected tree. This helper did not apply Git ignore rules or decide that a dot-prefixed file was private.
| Needed layout | root_dir | base_dir |
|---|---|---|
| project/readme.txt inside the ZIP | Parent of project | project |
| readme.txt at the ZIP’s top level | The project directory | . |
Give base_dir relative to the chosen root
The current documentation specifies base_dir relative to root_dir. An absolute input path in that position is not a substitute for designing the member layout. Choose the tree first, then inspect the stored names. The outer ZIP filename says nothing about whether its contents begin with project/, input/project/ or some unintended prefix.
Use an output directory outside the tree being archived. That keeps the generated archive from becoming an unintended input in workflows that scan the same tree. Review the selected subtree for all intended files rather than assuming the helper knows your publishing or sharing policy.
The format supplies the extension
base_name is the output name without its format extension. Our separate test supplied a base name ending in already.zip and received already.zip.zip. Use a base such as output/flat with format zip instead of appending the extension twice.
Also check the returned path rather than inferring it from the current directory. The output path and the archive’s internal root are different decisions, and a process’s working directory can make a relative output name point somewhere unexpected.
Verify contents before handing off the archive
- Read the returned archive path and confirm the intended output directory.
- Open the ZIP and list every stored member, including hidden-looking names.
- Compare the expected prefix and payload names.
- Read representative payloads, or compare all required bytes when exact fidelity matters.
- Stop the handoff if extra files, missing content or the wrong root appear.
Our fixture checked every payload, not only whether the ZIP could be opened. A valid archive can still package the wrong tree. This guide does not provide a sanitizer for untrusted extracted archives or a backup metadata guarantee.
If the problem is merging files into an existing destination before packaging, the copytree merge guide addresses that separate operation. Copying successfully does not establish that the next archive has the intended member root.
Method: current Python 3.13 documentation, a historical archive-root question and three original ZIP cases under Python 3.13.18/macOS. No private folder, actual publication package, extraction security test or alternate archive implementation was tested.