A BitTorrent client written from scratch in Python — no third-party BitTorrent or bencode libraries. It parses .torrent files, announces to HTTP trackers, performs the peer wire protocol handshake, and downloads and verifies real file data from peers on the live BitTorrent network — including multi-file torrents.
This project started from CodeCrafters' "Build Your Own BitTorrent" challenge and was substantially refactored afterward: the original single-file, copy-pasted implementation was split into focused modules, a from-scratch bencode encoder was added (the original relied on a third-party library for encoding), the CLI was rebuilt on argparse instead of manual sys.argv parsing, and a full pytest test suite was added alongside peer retry/failover and multi-file torrent support.
- Decodes and encodes bencode, BitTorrent's serialization format, entirely from scratch
- Parses
.torrentfiles and computes the info hash, for both single-file and multi-file torrents - Announces to an HTTP tracker and retrieves a peer list
- Performs the BitTorrent peer handshake and message exchange (bitfield, interested, unchoke), with a real timeout on every socket read
- Tries every peer in the list in order — if one fails to connect or refuses to cooperate, it automatically moves on to the next
- Downloads individual pieces or entire torrents, verifying each piece's SHA1 hash against the
.torrentfile's metadata before accepting it - Reconstructs the correct folder/file hierarchy on disk for multi-file torrents
- Backed by a
pytesttest suite covering the bencode codec, torrent parsing, peer protocol edge cases, and file-writing logic
| Module | Responsibility |
|---|---|
bencode.py |
Bencode decoding and encoding — no dependencies |
torrent.py |
Torrent class: parses a .torrent file into a reusable object (announce URL, length, piece length, piece hashes, info hash, and file list for multi-file torrents) |
tracker.py |
Announces to the tracker over HTTP and parses the compact peer list |
peer.py |
The peer wire protocol — handshake, unchoke sequence with per-message timeouts, block-level piece requests with hash verification, and peer retry/failover |
cli.py |
Command-line entry point (argparse), plus reconstruction of downloaded bytes into single or multiple files on disk |
Each module only depends on the one(s) below it in this table, so each can be tested — and was tested — in isolation.
Requires Python 3.14+ and the requests library.
pip install requests(or uv sync, if using the included uv.lock)
Run all commands from the repository root.
Decode a raw bencoded value:
python3 app/cli.py decode "d3:foo3:bare"Show a torrent's metadata:
python3 app/cli.py info sample.torrentList peers from the tracker:
python3 app/cli.py peers sample.torrentDownload a single piece:
python3 app/cli.py download_piece -o piece0.bin sample.torrent 0Download a full torrent:
python3 app/cli.py download -o output.bin sample.torrentFor a multi-file torrent, -o is treated as a destination folder — the torrent's own name (from its metadata) becomes the actual folder created inside it, so downloading multiple torrents to the same -o never collides.
pip install pytest
pytest tests/| Test file | Covers |
|---|---|
test_bencode.py |
Decode/encode correctness for every bencode type, plus a full round-trip test |
test_torrent.py |
Regression tests against a real sample .torrent, plus multi-file length/files calculations |
test_peer.py |
waitForUnchoke's three real behaviors — success, choke, and unresponsive peer — using a fake socket, no real network needed |
test_cli.py |
File-writing logic for both single-file and multi-file torrents, using real temporary directories |
This is a functional client, not a production-grade one. Notable gaps, roughly in order of impact:
- Single peer per download — no concurrent multi-peer downloading, so speed is limited to one connection; retry/failover currently only applies when establishing a connection, not if a peer drops mid-download
- No request pipelining — blocks are requested one at a time, waiting for each response before sending the next
- HTTP trackers only — no UDP tracker support, and no fallback across an
announce-list - No magnet link support —
.torrentfiles only - Download only — the client doesn't seed/upload pieces to other peers
- Two unrelated hardcoded peer IDs — the tracker announce and the peer handshake use different values rather than one shared identity; harmless today, but worth unifying
- Concurrent downloading across multiple peers, with pipelined block requests
- Peer failover mid-download, not just at initial connection
- UDP tracker support
- Rarest-first piece selection
- Magnet link support (BEP 9 metadata exchange)