Python Challenge - Level 6
Problem

The very first line of the HTML source is the hint:
<html>
<!-- <-- zip -->
</html>
The URL path is channel — change it to channel.zip to download a ZIP archive.
Inside the ZIP, each entry's comment field contains the name of the next entry to visit — a linked list hidden in ZIP metadata. Follow the chain from entry 90052, collecting each comment along the way. The concatenated comments spell the answer.
Solution
Python
zipfile.ZipFile gives direct access to per-entry comments via ZipInfo.comment:
import urllib.request, zipfile, io
data = urllib.request.urlopen(
"http://www.pythonchallenge.com/pc/def/channel.zip"
).read()
with zipfile.ZipFile(io.BytesIO(data)) as zf:
name = "90052"
comments = []
while True:
info = zf.getinfo(name + ".txt")
next_name = zf.read(name + ".txt").decode().strip()
comments.append(info.comment.decode())
if not next_name.isdigit():
print("Final content:", next_name)
break
name = next_name
print("".join(comments)) # hockey
Go
Go's archive/zip exposes zip.File.Comment per entry:
package main
import (
"archive/zip"
"bytes"
"fmt"
"io"
"net/http"
"strings"
)
func main() {
resp, _ := http.Get("http://www.pythonchallenge.com/pc/def/channel.zip")
data, _ := io.ReadAll(resp.Body)
resp.Body.Close()
r, _ := zip.NewReader(bytes.NewReader(data), int64(len(data)))
// Build a lookup map: filename → *zip.File
files := make(map[string]*zip.File)
for _, f := range r.File {
files[f.Name] = f
}
name := "90052.txt"
var comments strings.Builder
for {
f := files[name]
comments.WriteString(f.Comment)
rc, _ := f.Open()
body, _ := io.ReadAll(rc)
rc.Close()
next := strings.TrimSpace(string(body))
if next == "" || !isDigit(next) {
fmt.Println("Final:", next)
break
}
name = next + ".txt"
}
fmt.Println(comments.String()) // hockey
}
func isDigit(s string) bool {
for _, c := range s {
if c < '0' || c > '9' {
return false
}
}
return true
}
Rust
use std::io::{Cursor, Read};
use zip::ZipArchive;
fn main() {
let bytes = reqwest::blocking::get(
"http://www.pythonchallenge.com/pc/def/channel.zip"
).unwrap().bytes().unwrap();
let mut archive = ZipArchive::new(Cursor::new(bytes)).unwrap();
let mut name = "90052.txt".to_string();
let mut comments = String::new();
loop {
let mut file = archive.by_name(&name).unwrap();
comments.push_str(file.comment());
let mut content = String::new();
file.read_to_string(&mut content).unwrap();
let next = content.trim().to_string();
if next.is_empty() || !next.chars().all(|c| c.is_ascii_digit()) {
println!("Final: {}", next);
break;
}
name = format!("{}.txt", next);
}
println!("{}", comments); // hockey
}
Language Comparison
| Feature | Python | Go | Rust |
|---|---|---|---|
| ZIP reading | zipfile.ZipFile — stdlib |
archive/zip — stdlib |
zip crate (external) |
| Per-entry comment | ZipInfo.comment (bytes) |
zip.File.Comment (string) |
ZipFile::comment() (string) |
| In-memory ZIP from bytes | io.BytesIO(data) |
bytes.NewReader(data) |
Cursor::new(bytes) |
| Entry lookup by name | zf.getinfo(name) — O(n) scan |
Build a map[string]*zip.File |
archive.by_name(name) — O(n) scan |
Key insight: All three languages can parse ZIP files from memory without writing to disk — using Python's BytesIO, Go's bytes.Reader, or Rust's Cursor<T>. The ZIP spec has a per-entry comment field (separate from the file content) that is rarely used in practice — this puzzle is a great reminder it exists.
Next Level
http://www.pythonchallenge.com/pc/def/hockey.html → redirects to ../return/good.html

