Weeks 1 through 5 had a new concept every day.
Week 6 was different. One project. Six days. Start to finish.
The goal: build a URL shortener — something like bit.ly — entirely from scratch, in Python, without any web framework, database, or tutorial to follow. Design it first, build it, add analytics, write a README, ship it to GitHub, share it publicly, and improve it from feedback.
This week taught me more about how software actually gets made than any single concept lesson so far.
Day 36 — Design Before Code
The first day had no code at all.
Before touching the editor, I had to answer six questions on paper and write them up as a design document. This felt strange. I wanted to start building. But forcing myself to write Design.txt first changed how confidently I coded for the rest of the week.
Design Doc For URL Shortner
1. What it does?
-> The URL Shortner takes long URLs for e.g. https://www.examplesite.com/s?k=#435
and converts it into a Short URL for e.g. bit.ly/5345. The URL shortner
also records the date of when the URL was generated.
2. The Data Model for each URL:
-> Each URL Will be stored as:
{
"long_url": "https://www.examplesite.com/s?k=#435",
"short_code": "5345",
"created_at": "2026-11-21"
}
3. API (What users can do):
POST/long_URL(Server takes the long url and generates a short code)
GET/short_code(When a user visits the short URL, the system looks up the code
and redirects them to the original long URL.)
Basically this is what user can do:
1. Shorten a URL
User sends: {"url": "https://very-long-link.com"}
System returns: {"short_url": "my.ly/abc123"}
2. Visit a short URL
User clicks my.ly/abc123
System looks up abc123, finds the long URL, sends user there
4. Storage method:
-> I am using JSON as a Storage method because, I am familiar with it and I
have used JSON format in many projects. I know there are other efficient ways
like using a database but, I am more familiar with JSON's.
5. Short Code Generation:
-> I wil use Python's random module to generate 6 characters
I will check if the code already exists
if the code exists I will generate it again.
6. Edge Cases:
-> Duplicate Code: Check before Saving
Missing Code: Return "URL not found" message
Invalid URL: Check if the URL starts with "http"
Duplicate Long URL: If the same long URL is submitted again, I will return
the existing short code instead of creating a new one.
Six questions. Six answers. Before writing a single line of code.
What this gave me: I knew exactly what classes I needed, what data I was storing, what edge cases to handle, and what the user experience would feel like — before I had to think about syntax. Every day after this one was faster because of it.
The "API" section isn't a real HTTP API — this is a CLI tool. But thinking in terms of POST (shorten a URL) and GET (retrieve the original) gave the design clarity that made the functions obvious when I got there.
Day 37 — Building the CLI: JSON Storage & Core Logic
Day 37 was the main build. Everything in the design doc became Python.
The whole project lives in one class — Url — plus two standalone helper functions for reading and writing the JSON file. Here's the complete code:
import json
import random
import time
import os
import csv
class Url:
def __init__(self, long_url):
self.long_url = long_url
self.short_code = None
self.visit_counter = None
self.created_at = time.ctime()
def store_data(self):
'''Saves the long_url, short_code, created_at in a dictionary'''
data = {
"long_url": self.long_url,
"short_code": self.short_code,
"created_at": self.created_at
}
return data
def visit(self):
'''implements the notion of clicking on the url'''
self.visit_counter = 0
while True:
user_response = input("Do you want to visit the short URL?(yes/no): ").strip().lower()
if user_response in ("yes", "no"):
break
if user_response == "yes":
self.visit_counter += 1
return self.visit_counter
else:
return
def most_clicked(self, url):
'''Stores the click counter with its long url in a csv file'''
if not self.visit():
report_data = []
if os.path.exists("clicked_report.csv"):
with open("clicked_report.csv") as data_file:
reader = csv.DictReader(data_file)
for row in reader:
report_data.append(row)
for row_data in report_data:
if row_data['url'].strip() == url:
row_data['clicks'] = str(int(row_data['clicks']) + 1)
with open("clicked_report.csv", "w", newline = "") as edit_file:
file_writer = csv.DictWriter(edit_file, fieldnames = ["url", "clicks"], lineterminator = "\n")
file_writer.writeheader()
file_writer.writerows(report_data)
return
with open("clicked_report.csv", "a", newline = "") as report_file:
writer = csv.DictWriter(report_file, fieldnames = ["url", "clicks"], lineterminator = "\n")
writer.writeheader()
writer.writerow({"url": url, "clicks": self.visit_counter})
def generate_code(self):
urls = retrieve_urls()
ls = "abcdefghijklmnopqrstuvwxyz0123456789"
while True:
code = ""
for _ in range(6):
code += random.choice(ls)
if code not in urls:
self.short_code = code
return self.short_code
def shorten_url(self):
data = retrieve_urls()
for code, info in data.items():
if info['long_url'] == self.long_url:
self.short_code = info['short_code']
print(f"URL already exists: bit.ly/{self.short_code}")
self.visit()
self.most_clicked(self.long_url)
return
self.generate_code()
print(f"bit.ly/{self.short_code}")
self.visit()
self.most_clicked(self.long_url)
#Save to file
data[self.short_code] = self.store_data()
store_urls(data)
def main():
while True:
user_input = input("Enter URL: ").strip()
if user_input.startswith("https://") or user_input.startswith("http://"):
break
url = Url(user_input)
url.shorten_url()
def store_urls(url_data):
with open("urls.json", "w") as file:
json.dump(url_data, file, indent = 4)
def retrieve_urls():
if os.path.exists("urls.json"):
with open("urls.json") as urls_file:
return json.load(urls_file)
else:
return {}
if __name__ == "__main__":
main()
Four methods, two helpers, one data file. Let me walk through the logic of each piece.
generate_code()
ls = "abcdefghijklmnopqrstuvwxyz0123456789"
while True:
code = ""
for _ in range(6):
code += random.choice(ls)
if code not in urls:
self.short_code = code
return self.short_code
36 characters (26 letters + 10 digits), 6 positions: that's 36⁶ = 2,176,782,336 possible codes. The while True loop keeps generating until it finds one that doesn't already exist in urls.json. In practice, for any reasonable number of URLs, this will succeed on the first try every time — but the collision check is there because the design doc said it should be.
shorten_url() — deduplication
for code, info in data.items():
if info['long_url'] == self.long_url:
self.short_code = info['short_code']
print(f"URL already exists: bit.ly/{self.short_code}")
return
Before generating anything, scan every stored URL and check if this long URL already has a code. If it does, return the existing one instead of creating a duplicate. This was edge case #4 from the design doc, and handling it here meant the rest of the program never has to think about it.
store_urls() and retrieve_urls()
def store_urls(url_data):
with open("urls.json", "w") as file:
json.dump(url_data, file, indent = 4)
def retrieve_urls():
if os.path.exists("urls.json"):
with open("urls.json") as urls_file:
return json.load(urls_file)
else:
return {}
These are standalone functions rather than class methods — they don't belong to any single URL, they belong to the whole storage system. Keeping them outside the class makes that boundary clear. retrieve_urls() returns an empty dict if the file doesn't exist yet, so the first run works without any setup.
The stored JSON looks like this:
{
"a3k9m2": {
"long_url": "https://very-long-link.com/page?id=12345",
"short_code": "a3k9m2",
"created_at": "Wed Jun 10 14:32:05 2026"
}
}
Day 38 — Analytics: Tracking Clicks in CSV
Day 38 added the analytics layer. The most_clicked() method and visit() work together to track how many times each URL is visited and write that data to clicked_report.csv.
The most_clicked() method handles three scenarios:
-
First ever click on any URL —
clicked_report.csvdoesn't exist yet, so create it and write the first row - URL already in the report — read the file, find the row, increment the click count, rewrite the file
- New URL, report already exists — append a new row
def most_clicked(self, url):
'''Stores the click counter with its long url in a csv file'''
if not self.visit():
report_data = []
if os.path.exists("clicked_report.csv"):
with open("clicked_report.csv") as data_file:
reader = csv.DictReader(data_file)
for row in reader:
report_data.append(row)
for row_data in report_data:
if row_data['url'].strip() == url:
row_data['clicks'] = str(int(row_data['clicks']) + 1)
with open("clicked_report.csv", "w", newline = "") as edit_file:
file_writer = csv.DictWriter(edit_file, fieldnames = ["url", "clicks"], lineterminator = "\n")
file_writer.writeheader()
file_writer.writerows(report_data)
return
with open("clicked_report.csv", "a", newline = "") as report_file:
writer = csv.DictWriter(report_file, fieldnames = ["url", "clicks"], lineterminator = "\n")
writer.writeheader()
writer.writerow({"url": url, "clicks": self.visit_counter})
The click report ends up looking like this:
url,clicks
https://very-long-link.com/page?id=12345,3
https://github.com/Omk4314/progress-on-python,1
The read-modify-rewrite pattern for the CSV is the same one from Week 4's expense tracker — read everything into memory as a list of dicts, find the row to change, update it in memory, rewrite the whole file. That pattern showing up again in a completely new project confirmed that it had actually stuck.
Day 39 — README & GitHub
Day 39 was about making the project usable by someone who has never seen your code.
Writing a good README is harder than it sounds. I had to think about:
- What does this do? (for someone who doesn't know me)
- How do I run it? (exact commands, step by step)
- What will they see? (concrete example output)
- What format is the data stored in?
The README I wrote covers all of that. Here's the usage section:
$ python url_shortener.py
Enter URL: https://very-long-link.com/page?id=12345
bit.ly/a3k9m2
Do you want to visit the short URL?(yes/no): yes
And the data storage format section shows exactly what goes into urls.json and clicked_report.csv — so anyone reading the README knows what to expect without running the code first.
The repository is live:
👉 github.com/Omk4314/progress-on-python
Day 40 — Sharing & Getting Feedback
Day 40 was the most uncomfortable day of the whole month.
I posted the project publicly and asked strangers to review my code. Reddit's r/learnpython, Twitter, Discord communities. The verification criterion for this day: at least 3 people reviewed your code and you fixed something.
What I was nervous about: people pointing out everything wrong.
What actually happened: people were genuinely helpful.
The feedback I received:
- The
most_clicked()logic was fragile — ifvisit()returnedNone(user said "no"), theif not self.visit()check would callvisit()again, prompting the user a second time. The condition and the call should be separated. - The
Design.txtfile being in the root directory instead of inside theurl-shortner/folder was inconsistent. - Suggested adding a
list all URLscommand and adeletecommand to make it a fuller CLI tool.
Sharing publicly when you're a beginner is uncomfortable. But the code review feedback was more precise and more useful than anything I could have gotten from reviewing my own work. Other people see things you've been looking at too long to notice.
Day 41 — Refactoring From Feedback
Day 41 was about taking the feedback and making the code better.
The most important fix: the double-call bug in most_clicked(). The original code called self.visit() inside the condition and then relied on self.visit_counter being set, but if the user said "no" to visiting, visit() returned None and the condition if not self.visit() triggered — which called visit() again. The fix is to call visit() once, store the result, and branch on that:
# Before (buggy)
if not self.visit():
# ...
# After (correct)
clicked = self.visit()
if clicked:
# user said yes — counter is set
else:
# user said no — update existing record
I also added if __name__ == "__main__": to the main entry point — it was already there but I made sure it was at the very bottom of the file where it belongs, not accidentally inside a block.
The refactor made me re-read the whole file carefully for the first time since writing it. That rereading itself was valuable — I spotted two places where variable names were inconsistent and fixed those too.
📁 What I Built This Week
| Project | File | Concepts Used |
|---|---|---|
| Design Document | url-shortner/Design.txt |
Planning, data model, API design, edge cases |
| URL Shortener | url-shortner/url_shortner.py |
OOP, JSON storage, CSV analytics, random, time, os
|
| README | url-shortner/README.md |
Documentation, usage examples, setup instructions |
All the code is on GitHub — every week, every file:
👉 github.com/Omk4314/progress-on-python
What Actually Clicked This Week
-
Design before code is not wasted time. Every function I wrote on Day 37 mapped directly to something in
Design.txt. When you know what you're building, the syntax becomes the easy part. - Edge cases are features, not afterthoughts. Writing them down on Day 36 meant they were handled on Day 37 — not discovered on Day 40 when someone else ran the program.
- One project for a whole week goes deeper than one concept per day. By Day 39 I understood every line of the file. By Day 41 I could explain why each decision was made.
- Public code review is uncomfortable and worth it. Nobody was cruel. Everyone was constructive. And the double-call bug I wouldn't have caught alone got spotted immediately.
-
time.ctime()gives a human-readable timestamp in one call. Small find, genuinely useful. - 36⁶ possible codes is more than 2 billion. I worked that out while writing the design doc. It's the kind of thinking you only do when you plan before building.
What I Want to Learn Next
Week 7 has some clear targets:
-
pytest— I want to write real automated tests, not justmain()with print statements. The refactor week taught me how easy it is to break things you thought were working. - APIs — the URL shortener is a CLI tool now. The natural extension is making it serve HTTP requests. That means Flask or FastAPI, which means web.
- Databases — I chose JSON for storage because I know it. But the design doc itself acknowledged there are better options. SQLite is the obvious next step.
-
Virtual environments and
pipproperly — the URL shortener uses only standard library modules, but the moment I add Flask that changes.
Six weeks in. I've gone from "Hello, World!" to a working URL shortener with click analytics and a public README. The gap between where I started and where I am now is genuinely hard to believe from the inside.
If you've built something this week — anything — put it in the comments. A URL shortener, a to-do app, a calculator. Share it. Ask for feedback. The worst that happens is someone helps you.
See you in Week 7. 🐍
Week 6 complete. First real project shipped. Code review survived. Still going.
Top comments (0)