Weeks 1 and 2 were about learning the language.
Week 3 was about learning how to hold data.
Every meaningful program stores and organises information — lists of tasks, books of contacts, inventories of products. This week I stopped treating data as a side effect of my programs and started designing programs around data. Lists, dictionaries, nested structures, sets, tuples — six days that changed how I think about writing Python.
Here's what happened.
Day 15 — Lists Deep Dive: A Real To-Do App
Day 15 started with a question: what's the most practical thing I can build with a list?
A to-do app. Add tasks, remove them, mark them complete, filter by status. I built the whole thing using list methods — append(), remove(), pop(), index() — and kept the clean one-function-one-job structure I picked up in Week 2:
import sys
pending_tasks = []
completed_tasks = []
def main():
print("\n\tWelcome to the app!")
while True:
while True:
usr = input('''What would you like to do?(choose the corresponding number)\n1.Add Tasks
\n2.Mark complete\n3.View Tasks\n4.Remove Task\n5.Press 'q' to quit\n-> ''')
if usr == "1" or usr == "2" or usr == "3" or usr == "4" or usr == "q":
break
match usr:
case "1":
add_task()
case "2":
mark_complete()
case "3":
filters()
case "4":
remove_task()
case "q":
sys.exit()
#Add
def add_task():
'''Adds Task to pending_tasks'''
new_task = input("Enter the new task: ").strip()
pending_tasks.append(new_task)
print("Task Added")
print_tasks(pending_tasks)
#Remove
def remove_task():
'''Removes the Task mentioned by the user'''
rmove = input("Enter the task you want to remove: ").strip()
if rmove in pending_tasks:
pending_tasks.remove(rmove)
print_tasks(pending_tasks)
elif rmove in completed_tasks:
completed_tasks.remove(rmove)
print_tasks(completed_tasks)
else:
print("Task not found!")
#Mark Complete
def mark_complete():
'''Transfers the task completed by the user from pending_tasks to completed_tasks'''
complete = input("Enter the task you want to mark complete: ").strip()
if complete in pending_tasks:
completed_tasks.append(pending_tasks.pop(pending_tasks.index(complete)))
elif complete in completed_tasks:
print("Task already completed!")
else:
print("Task not Found!")
#Filter by status
def filters():
'''Let's user filter between pending tasks and completed tasks'''
filter_task = input("Which tasks do you want to see?(Pending/Completed): ").strip().lower()
if filter_task == "pending":
if pending_tasks:
print_tasks(pending_tasks)
else:
print("No Task Found!")
elif filter_task == "completed":
if completed_tasks:
print_tasks(completed_tasks)
else:
print("No Task Found!")
else:
print("Invalid input!")
# print tasks
def print_tasks(t):
'''prints the list of tasks to the screen'''
for tsk in t:
print(tsk)
#calling the main function
main()
The line that clicked hardest: completed_tasks.append(pending_tasks.pop(pending_tasks.index(complete))). That single chain finds the task by name, removes it from pending, and returns it — so it lands straight into completed_tasks. One line doing exactly the right job. That's when list methods started feeling like a real toolkit rather than a list of things to memorise.
Day 16 — List Comprehensions: Less Code, Same Power
Day 16 introduced one of Python's most beloved features — list comprehensions. Build a filtered or transformed list in a single, readable line instead of writing a full loop.
I built a number filter that splits 1–10 into even, odd, and prime — with a custom is_prime() function handling the prime logic:
def main():
numbers = list(range(1,11))
even_numbers = [num for num in numbers if num % 2 == 0]
odd_numbers = [num for num in numbers if num % 2 != 0]
prime_numbers = [num for num in numbers if is_prime(num)]
print(even_numbers)
print(odd_numbers)
print(prime_numbers)
def is_prime(n):
if n < 2:
return False
else:
for i in range(2, int(pow(n, 0.5)) + 1):
if n % i == 0:
return False
return True
main()
Output:
[2, 4, 6, 8, 10]
[1, 3, 5, 7, 9]
[2, 3, 5, 7]
Two things hit me at once here:
-
List comprehensions read like sentences: "give me every
numfromnumbersif it's even." Once you see it, you can't unsee it — a regularforloop starts feeling like too many words. -
The prime checker uses
pow(n, 0.5)— the square root ofn— to limit how many divisors it needs to test. That's a genuine algorithmic optimisation. Week 1 me would have looped all the way ton. Discovering that while building felt like a small unlock.
Day 17 — Dictionaries: A Contact Book
Dictionaries were the data structure I'd been reaching for since Week 1. Key-value pairs — the natural way to model real-world objects.
I built a contact book: name as the key, a nested dictionary of details as the value. Add contacts, view them, delete them:
#building a contact book
# contact -> information = mobile number, email, address
import sys
contacts = {}
app_running = True
def main():
'''main function'''
global app_running
while app_running:
print("\n\tWELCOME TO CONTACT BOOK\n")
while True:
ask_user = input('''What would you like to do?\n
(Choose the corresponding number of the task you want to perform)\n
1. Add/Save new contact\n
2. View contacts\n
3. Delete contact\n
(press 'q' to quit)\n -> ''').strip().lower()
if ask_user == "1" or ask_user == "2" or ask_user == "3" or ask_user == "q":
break
match ask_user:
case "1":
save_contact()
case "2":
show_contacts()
case "3":
delete_contact()
case "q":
sys.exit()
def save_contact():
'''Saves user's contact'''
global app_running
while app_running:
if not app_running:
break
name = input("Enter name: ")
user = {}
contacts[name] = user
while True:
while True:
user["mob"] = input("Enter Mobile No.: ").strip()
user["email"] = input("Enter email: ").strip()
if user["mob"].isdecimal():
break
else:
print("Invalid Mobile No.")
if "@" in user["email"]:
break
else:
print("Invalid email!")
user["add"] = input("Enter address: ").strip()
print("Contact saved...\n")
show_contacts()
break
while True:
response = input("Do you want to save another contact?(yes/no)? ")
if response == "yes" or response == "no":
break
if response == "no":
break
def show_contacts():
'''Shows contact's of the user'''
if contacts:
print_contact()
else:
print("No Contacts found!")
def delete_contact():
'''Deletes a contact'''
show_contacts()
delete = input("Enter the name of the you want to delete: ")
if delete in contacts:
del contacts[delete]
else:
print("No contact found!")
def print_contact():
'''prints contacts'''
for contact, user_info in contacts.items():
print(f"Contact name: {contact}\nPhone No.: {user_info['mob']}\nEmail: {user_info['email']}\nAddress: {user_info['add']}\n")
#Calling the main function
main()
The structure that made this work: contacts[name] = user where user is itself a dictionary of mob, email, and add. That's a dictionary inside a dictionary — my first proper nested data structure, and it arrived naturally because the problem required it.
.items() in print_contact() was also new — iterating over both keys and values simultaneously instead of just one. Once you know it exists, you can't stop using it.
Day 18 — Nested Data: An Inventory System
The contact book was one level of nesting. Day 18 went three levels deep: category → item → details:
#Building an inventory system
#category-> item-> details
global price
inventory = {
"Electronics": {
"Laptop": {"quantity": 10, "price": 999.99, "supplier": "TechCorp"},
"Mouse": {"quantity": 50, "price": 25.50, "supplier": "GadgetInc"}
},
"Furniture": {
"Chair": {"quantity": 20, "price": 150.00, "supplier": "ComfortCo"}
}
}
def main():
cat = input("Enter the category you want the total value of (furniture/electronics): ").title()
total_value(inventory[cat])
def total_value(inventory_item):
products = {}
products = inventory_item
total = 0
for product_info in products.values():
total += product_info["quantity"] * product_info["price"]
print(total)
main()
Ask for Electronics: (10 × 999.99) + (50 × 25.50) = 11,274.90.
Ask for Furniture: 20 × 150.00 = 3,000.00.
What struck me: I spent more time thinking about how to structure the dictionary than writing the functions. The shape of the data determined the shape of the code. That shift — designing data first, then writing logic around it — felt like a genuinely new layer of thinking.
Day 19 — Sets & Tuples
Day 19 was the two data structures I hadn't touched yet.
Tuples — like lists, but immutable. Once created, they can't be changed. Perfect for data that must stay fixed: config values, coordinates, question banks. The quiz project on Day 20 stores all questions and answers as tuples for exactly this reason — nothing can accidentally overwrite them mid-run.
Sets — unordered collections with no duplicates. The killer use case: deduplication. Have a list and want only the unique values? Convert to a set. Sets also have significantly faster membership checks than lists for large collections.
The mental model I settled on by end of day:
- Need order + mutability? → List
- Need key-value lookup? → Dictionary
- Need immutable, fixed data? → Tuple
- Need uniqueness + fast lookup? → Set
Day 20 — The Week 3 Project: A Python Quiz App
The end-of-week project brought together everything from the week — dictionaries, tuples, file I/O, try/except, randomisation, and a proper multi-file structure. The most architecturally considered thing I've built.
Two files: qdata.py holds all the quiz data, quiz.py runs the game.
qdata.py — The Data Layer
questions = (
"How many parameters does a print statement have?",
"Which functions returns the ASCII number associated with a char?",
"What does the method dict.keys() return?",
"What is the difference between a list and a tuple?",
"What is the full form of json?",
"What does the function json.dumps() does?",
"What does the function json.loads() does?",
"In a dictionary, what does (dict_name['key']) this syntax return?",
"Methods and functions are the same.",
"What type of datatype does the input() function return?",
)
answers = (
"3",
"ord()",
"view object",
"A list can be modified but a tuple cannot be modified",
"Javascript Object Notation",
"Converts dictionary to a str",
"Converts str to a dictionary",
"Returns the value associated with the given key",
"False",
"str",
)
qna = {}
for i in range(len(questions)):
qna[questions[i]] = answers[i]
options = [
["1", "2", "3", "4"],
["list()", "chr()", "ord()", "int()"],
["tuple of keys", "list of keys", "view object", "object"],
["They are both the same", "A list can be modified but a tuple cannot be modified", "A tuple can be modified but a list cannot be modified", "We can access an element in lists but not in tuples"],
["Java Subject Object Notes", "Javascript Object Notation", "Jackle and Son Oxygen Needs", "Java SON"],
["Closes the file", "Converts str to a dictionary", "Converts dictionary to a str", "Clears the file"],
["Creates a new file", "Converts dictionary to a str", "Converts str to a dictionary", "Opens the file"],
["Returns the key", "Returns the value associated with the given key", "Returns all the keys", "Returns the dictionary name"],
["True", "False", "Both True and False", "None of the above"],
["float", "integer", "character", "str"]
]
markers = ["A", "B", "C", "D"]
Questions and answers are tuples — immutable, so nothing can accidentally overwrite them at runtime. The qna dictionary maps each question directly to its correct answer, built dynamically with a for loop. Clean and easy to extend — add a question to questions, an answer to answers, options to options, and the game picks it up automatically.
quiz.py — The Game Engine
from pathlib import Path
import random
import qdata
import sys
global val
global score
num = list(range(0, 10))
file = Path("score.txt")
def main():
while True:
ask_user = input("Wanna play the fun python quiz?(yes/no) ").strip().lower()
if ask_user == "yes" or ask_user == "no":
break
if ask_user == "yes":
while True:
score = 0
random.shuffle(num)
for n in num:
val = n
marked_options = dict(zip(qdata.markers, qdata.options[val]))
display_question(val)
display_option(marked_options)
score = user_choice(val, score, marked_options)
high_score(score)
while True:
usr = input("Wanna play again(yes/no)? ").strip().lower()
if usr == "yes" or usr == "no":
break
if usr == "no":
sys.exit()
else:
sys.exit()
def display_question(val):
print(qdata.questions[val])
def display_option(marked_options):
for marker,option in marked_options.items():
print(f"{marker}. {option}")
def user_choice(val, score, marked_options):
while True:
try:
user_ans = input("Choose your option: ").strip().upper()
if marked_options[user_ans] == qdata.qna[qdata.questions[val]]:
score += 4
print("You got it right!")
else:
score -= 1
print("You are wrong:(")
print(f"Current Score: {score}")
return score
except KeyError:
print("Choose A/ B/ C/ D!!")
def high_score(score):
h_score = 0
if h_score < score:
h_score = score
file.write_text(f"High score: {h_score}")
print(file.read_text())
else:
file.write_text(f"High scores: {h_score}")
main()
Everything I've learned in three weeks lives inside this project:
- Tuples for immutable question and answer data
-
Dictionaries for the
qnalookup table andmarked_optionsper question -
random.shuffle()so questions arrive in a different order every game -
dict(zip())to pair A/B/C/D markers to options on the fly -
try/except KeyErrorto catch invalid answers gracefully instead of crashing -
pathlib+ file I/O to persist the high score between sessions -
Multi-file structure —
quiz.pyimportsqdataas a module, keeping data and logic completely separate
My current high score saved to score.txt: 35.
📁 What I Built This Week
| Project | File | Concepts Used |
|---|---|---|
| To-Do List App | todo_list.py |
Lists, append, remove, pop, index, filtering |
| Number Filter | number_filter.py |
List comprehensions, prime algorithm, pow()
|
| Contact Book | contact.py |
Dictionaries, nested dicts, .items(), del
|
| Inventory System | inventory.py |
Nested dictionaries, .values(), data-first design |
| Python Quiz App |
python-quiz/quiz.py + qdata.py
|
Tuples, dicts, zip(), shuffle(), try/except, file I/O, multi-file modules |
All the code is on GitHub — every week, every file:
👉 github.com/Omk4314/progress-on-python
What Actually Clicked This Week
- Data structures aren't just storage — they're design decisions. Choosing between a list, dictionary, tuple, or set shapes how the entire program works.
- Nested dictionaries feel natural once you need them. The contact book required them. I didn't force the structure — the problem suggested it.
-
List comprehensions are more readable, not just shorter.
[num for num in numbers if is_prime(num)]reads like a plain description of what you want. -
Separating data from logic is powerful.
qdata.pyandquiz.pyas two files meant I could change every question without touching the game engine once. -
try/except KeyErroris the right tool when dictionary lookups might fail on bad user input. Cleaner than checkingif key in dictevery time.
What I Want to Learn Next
Week 4 is on the horizon and I already know what's coming:
- Object-Oriented Programming — classes and instances. The to-do app and contact book are straining against the limits of functions-and-globals. Objects feel like the natural next step.
- Inheritance — sharing behaviour without copying code
-
__str__and__repr__— making objects print like real things - Larger multi-file projects — the quiz app gave me a taste of real structure
Three weeks in. The programs are starting to look like software.
If you're following along or learning Python yourself, drop your projects in the comments — beginner work deserves to be seen.
See you in Week 4. 🐍
Week 3 complete. I finally understand why everyone says "it's all about the data structures."
Top comments (0)