duplicates
Scan for identical files (duplicates) in subdirectories.
Requirements
- Python >= 3.11
- POSIX (Linux, macOS); MS Windows is not supported.
Installation
$ uv tool install duplicates
Or, if you prefer pipx:
$ pipx install duplicates
Description
To find files with identical content, the given directories are scanned and files of the same size have their SHA-256 fingerprints compared. Two files with identical fingerprints are considered to have the same content. There is a tiny chance for two files with the same fingerprint to have different content, but that chance is very remote.
Large files (≥ 64 KiB) are first compared by a cheap "partial" SHA-256 over their first and last 4 KiB; only files that survive that prefilter are read in full. For collections of large near-duplicates (videos, archives) this avoids reading most of the data.
Symbolic links and hidden entries are ignored by default. This behavior can
be changed with the CLI options --follow / --hidden or the constructor
options ignore_symlinks / ignore_hidden.
CLI examples
Print a short command overview:
$ duplicates --help
Scan directories dirA, dirB and dirC and report identical files:
$ duplicates dirA dirB dirC
dirA/file01
dirA/file01.bak
dirB/file.bak
dirA/file02
dirB/file02~
The oldest file is printed without indent; identical files are listed indented by a tab. The oldest file is treated as the original.
If you are willing to take risks, you can delete all duplicates at once. I wouldn't dare, but you get the picture:
$ duplicates --dups-only dirA dirB | while read dups ; do xargs -0 rm $dups ; done
With --dups-only, all duplicates for one original are printed on a single
line separated by \0 (ASCII NUL).
For the fish shell the syntax is almost identical:
$ duplicates --dups-only dirA dirB | while read -la dups ; xargs -0 rm $dups ; end
JSON output
For scripted consumption, --json emits the full result on stdout
including a statistics block with counts and the scan's elapsed time:
$ duplicates --json dirA dirB
{
"scanned_paths": ["dirA", "dirB"],
"duplicates": [
{
"hash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 1234,
"files": [
{"path": "dirA/file01", "age": 1700000000.0},
{"path": "dirA/file01.bak", "age": 1710000000.0}
]
}
],
"statistics": {
"total_files": 12,
"unique_files": 10,
"duplicate_groups": 1,
"duplicate_copies": 1,
"duplicate_bytes": 1234,
"unreadable_files": 0,
"elapsed_seconds": 0.0123
}
}
--json is mutually exclusive with --dups-only and --summary.
Combine with --unique to also include the unique files in the output.
Progress on long scans
--verbose surfaces phase markers and per-file logs to stderr — useful
when running over a slow filesystem (SMB, large library) where the tool
might otherwise look stuck:
INFO: Scanning 1 path(s)...
INFO: Scanned 1284 file(s) so far...
INFO: Discovered 4012 file(s) in 3987 size group(s)
INFO: Partial-hashing 12 file(s)...
INFO: Partial-hashing /films/movie.mp4 (5.2 GiB)
INFO: Full-hashing 4 file(s)...
INFO: Full-hashing /films/movie.mp4 (5.2 GiB)
--debug adds per-directory and per-ignored-entry messages on top.
Python API
from duplicates import DupFinder
uniq, dups, unreadable = DupFinder().scan(".")
uniq is a list of unique FileEntry objects. dups is a list of duplicate
groups, where each group is a list of FileEntry objects with identical
content. Use entry.age to identify the oldest file in a group. unreadable
collects files that could not be fingerprinted (permission denied, I/O error);
they cannot be classified and are returned separately instead of being
silently dropped.
A FileEntry is a dataclass with the following fields:
path: apathlib.Pathsize: file size in bytesage: modification time in seconds (Unix time)hash: the SHA-256 fingerprint (Nonefor unique files where no hash was needed)
Progress messages are emitted via the logging module on the duplicates
logger; configure logging in your application to see them.
Development
$ uv sync
$ uv run pytest
$ uv run ruff check .
$ uv run basedpyright
Metadata
Release files for duplicates 0.4.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| duplicates-0.4.3.tar.gz | 9.2 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| duplicates-0.4.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 19.9 kB
Release files / duplicates-0.4.3.tar.gz
| Download URL | duplicates-0.4.3.tar.gz |
|---|---|
| Size | 9.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
041a3ee97761e3c529bce3bce5f3edb862ee76c8bb1a524328499bff8d671f2d
|
|
BLAKE2b-256 checksum How to use checksums |
1878b00af6eecb4a0f22bf8e9f8749ae247802dd1fd86e28898b348e560e90f1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / duplicates-0.4.3-py3-none-any.whl
| Download URL | duplicates-0.4.3-py3-none-any.whl |
|---|---|
| Size | 10.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
545e722ff8712bb217b7b609bfc3f1b3e00aec28c31987cdce325feee08807bc
|
|
BLAKE2b-256 checksum How to use checksums |
7b85796f1ead3847b68b604e4779a6ac01dd2a82456b989b2d66cfecf738436c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for duplicates 0.4.3
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.