Technology · PRACTICAL GUIDE
zipfile.Path.is_symlink() always False? Check the runtime and stored member type
Twenty local controls separate a Python 3.12 predicate stub, Unix-marked ZIP metadata, CRC integrity and what the standard extractor actually creates.

On CPython 3.12.14, a synthetic ZIP member marked as a Unix symbolic link existed and returned True from zipfile.Path.is_file(), yet is_symlink() returned False. Inspecting its stored mode identified the link. The False result did not prove that the archive contained only regular files.
Check the runtime and the archive’s metadata convention before changing the archive. Our 20 asserted controls used seven members in a 751-byte synthetic ZIP on Linux. They also showed that a successful CRC test and a successful extraction answer different questions from stored member-type inspection.
1. Test the implementation, not just method availability
Print sys.version and confirm that the object is zipfile.Path, rather than pathlib.Path referring to an already extracted file. On our actual 3.12.14 interpreter, inspection of zipfile.Path.is_symlink showed an implementation that simply returns False. The method was callable, so an attribute-existence test would not have caught the limitation.
Python’s versioned reference records the behavior change in 3.13: earlier is_symlink returned False unconditionally. We read that documentation but did not run 3.13. Probe your own deployed version with a known benign link-marked fixture; do not infer behavior from the latest documentation or a method name alone.
2. Inspect each member under an explicit metadata convention
Iterate the open archive’s infolist() and retain each ZipInfo. Record filename, create_system and the mode expression info.external_attr >>16. For the Unix-host convention tested here, require create_system==3 before interpreting that mode with stat.S_ISLNK, stat.S_ISREG or stat.S_ISDIR. These classify the stored type bits, not the current filesystem.
PKWARE defines external attributes as host-dependent. Our deliberately narrow classifier labels non-Unix hosts and modes without a recognized type as unknown. Unknown is not regular and not a clean bill of health. Other producers, including other host conventions, need their own verified handling rather than a forced negative answer.
3. Keep names, target text and type bits separate
In the fixture, alias and dangling were both link-marked even though dangling named an absent target. Reading those members returned target.txt and absent.txt as their stored payload text. An ordinary member named ordinary.link remained a regular file by its explicit Unix type bits. A filename suffix therefore did not establish link type.
The folder/ member had both a directory-style name and directory mode. ZipInfo.is_dir uses the name’s trailing slash; it is not the same test as examining every stored Unix type bit. Our permissions-only member had no recognized type and remained unknown. Permission bits alone did not justify labeling it a regular file.
4. Measure the actual extraction outcome separately
testzip() returned None for the entire fixture, meaning its tested members passed that CRC/header check. The link metadata was still present. Integrity checking had not answered whether a member was a symbolic link or whether another extractor would restore one.
We extracted only the benign alias member with Python’s standard ZipFile.extract into a fresh temporary directory. On this 3.12.14/Linux run, the output was a regular file containing the text target.txt, not an operating-system symlink. No external extractor was run. Do not generalize this output to other programs or rewrite metadata just to make a predicate return the answer you expected.
5. Set a completion test for the handoff you intend
Our 20 controls checked method availability, exists/is_file/is_symlink, the seven-member type matrix, dangling and suffix controls, unknown metadata, CRC success, stored target text, an absent archive entry, and the extracted file’s type and exact contents. The saved report includes the runtime, predicate source, script hash and archive hash.
For your own workflow, keep an unchanged archive and a small known fixture. Verify the member inventory and classify only the conventions your policy actually supports; investigate or reject unknown types according to that policy. Then check the intended extractor’s resulting object types and contents. This is a member-type diagnosis, not a complete untrusted-archive extraction validator or a claim of cross-platform compatibility.
