Python Modules, Dates, and Error Handling
The morning I forgot pip and a venv were different things
Weeks into practicing Python fundamentals, I thought I had a decent handle on things. Then I opened a notebook, typed python -m venv myenv into a code cell, and stared at a SyntaxError I genuinely didn't understand at first. It took me an embarrassing few minutes to realize I was trying to run a terminal command as if it were Python code. Small mistake, but it exposed a gap: I could write loops and functions just fine, but I hadn't actually internalized the difference between "code that runs inside Python" and "commands that manage Python from the outside." That mix-up, and the string of Colab-specific errors that followed it, ended up teaching me more than the exercises themselves. This article walks through that second stretch of practice: modules, dates, JSON, error handling, and virtual environments, along with the mistakes that made each concept actually stick.
Why this batch of practice mattered
The first week was about control flow — loops, conditionals, functions, basic data structures. This second set of tasks moved into something different: working with Python’s modules, the built-in and third-party tools that do the heavy lifting so you don’t reinvent things like date math or JSON parsing yourself. This is also where pip and virtual environments show up, since almost no real Python project runs on the standard library alone.
Working with the math module
Square root and factorial
import mathprint(math.sqrt(144)) # 12.0
print(math.factorial(6)) # 720math.sqrt() always returns a float, even when the answer is a whole number — worth remembering if you're ever comparing it directly to an integer. math.factorial() multiplies every whole number from 1 up to the number given, which is exactly the loop-based factorial program from week one, just handled for you.
Rounding with math
import mathnumber = 5.768
print(math.floor(number * 100) / 100) # 5.76, truncates
print(round(number, 2)) # 5.77, rounds properlyThis one caught me off guard. I assumed math would have an obvious "round to 2 decimals" function, but math.floor() and math.ceil() only truncate in one direction — they don't round to the nearest value. Python's built-in round(), which isn't part of the math module at all, is actually the right tool for normal rounding. Small distinction, easy to miss.
GCD, and π
import mathnum1 = int(input("Enter first number: "))
num2 = int(input("Enter second number: "))
print(math.gcd(num1, num2))import math
radius = 5
area = math.pi * radius ** 2math.gcd() finds the greatest common divisor without you having to write the Euclidean algorithm by hand, and math.pi gives π to enough decimal places for basically any geometry calculation you'd need.
Dates without the guesswork
Getting today’s date and time
from datetime import datetime, datenow = datetime.now() # date and time
today = date.today() # date only, no timeI’d been using datetime.now() for everything until I realized date.today() exists specifically for when you don't care about the clock time — which, it turns out, is most of the time.
Formatting dates
from datetime import datetime
print(datetime.now().strftime("%d-%m-%Y"))strftime() (string format time) is the function that turns a date object into whatever text format you want. %d, %m, and %Y stand for day, month, and four-digit year — get the order of those right and the rest is just picking your separators.
Age from birth year, and days until a birthday
from datetime import datetime, datebirth_year = int(input("Enter your birth year: "))
age = datetime.now().year - birth_yeardef days_until_birthday(birth_date_str):
birth_date = date.fromisoformat(birth_date_str)
today = date.today()
next_birthday = birth_date.replace(year=today.year)
if next_birthday < today:
next_birthday = next_birthday.replace(year=today.year + 1)
return (next_birthday - today).daysThe birthday calculator is the one that actually taught me something about date logic: you can’t just subtract two dates and assume the answer is positive. If someone’s birthday already passed this year, you have to roll it forward to next year before subtracting, or you’ll get a negative number of days. .replace(year=...) swaps out just the year on an existing date, which is the cleanest way to do that.
Days between two arbitrary dates
from datetime import datedate1 = date.fromisoformat(input("Enter first date (YYYY-MM-DD): "))
date2 = date.fromisoformat(input("Enter second date (YYYY-MM-DD): "))
print(abs((date2 - date1).days))Wrapping the subtraction in abs() means it doesn't matter which date the user enters first — you always get a positive count.
JSON: turning Python objects into text and back
import jsonstudents = [
{"name": "Alice", "age": 20, "grade": "A"},
{"name": "Bob", "age": 22, "grade": "B"},
]with open("students.json", "w") as f:
json.dump(students, f, indent=4)with open("students.json", "r") as f:
loaded = json.load(f)
The naming took me a moment to lock in: json.dumps() converts a Python object to a JSON string (the "s" is for string), while json.dump() writes it directly to a file. Same relationship on the reading side — json.loads() for a string, json.load() for a file. Mixing these up gives confusing errors that don't obviously point back to "wrong function."
Get Babatundenaheemot’s stories in your inbox
Join Medium for free to get updates from this writer.
One thing worth knowing: JSON has no concept of a tuple. Feed json.dumps() a tuple and it comes back as a JSON array (which looks identical to a list) — Python's tuple/list distinction just doesn't exist on the other side.
And when a JSON file has a syntax problem — a trailing comma, a missing quote — json.load() doesn't fail silently. It raises a json.JSONDecodeError, which is worth catching explicitly if you're reading files you didn't generate yourself.
try:
with open("bad_file.json") as f:
data = json.load(f)
except json.JSONDecodeError:
print("The file contains invalid JSON.")Error handling: catching problems before they crash the program
The basics
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero.")try:
value = int("abc")
except ValueError:
print("Cannot convert 'abc' to an integer.")The pattern is always the same: wrap the risky line in try, and catch the specific exception it might raise. Catching ZeroDivisionError and ValueError separately (rather than one bare except:) means you actually know which problem occurred, instead of just knowing "something" went wrong.
Looping until the input is actually valid
while True:
try:
num = int(input("Enter an integer: "))
break
except ValueError:
print("Invalid input. Please enter a valid integer.")This is the pattern I use now for basically any user-input program: wrap the input inside a while True loop, and only break out of it once the conversion succeeds. It's the difference between a program that crashes on the first typo and one that just asks again.
Bringing it together: a math quiz
import randomscore = 0
for i in range(5):
num1, num2 = random.randint(1, 20), random.randint(1, 20)
op = random.choice(['+', '-', '*'])
answer = eval(f"{num1}{op}{num2}")
try:
guess = int(input(f"{num1} {op} {num2} = "))
if guess == answer:
score += 1
except ValueError:
print("Skipped — invalid input.")This one combines almost everything from the two weeks — loops, conditionals, the random module, and try/except — into a single small program, which is really where these concepts start to feel less like isolated tasks and more like how you'd actually write something.
Where pip and virtual environments tripped me up
This was the messiest part of the whole practice set, and entirely because of where I was running things, not what I was typing.
python -m venv myenv
source myenv/bin/activate # macOS/Linux
myenv\Scripts\activate # Windows
pip install requestsThat’s the standard flow on a real machine. But I was working in Google Colab, and Colab’s Python setup doesn’t ensurepip cleanly inside a fresh venv, so the environment got created with no pip inside it — myenv/bin/pip: No such file or directory. The fix was creating the venv without pip, then bootstrapping pip manually:
!python3 -m venv myenv --without-pip
!curl -sS https://bootstrap.pypa.io/get-pip.py -o get-pip.py
!myenv/bin/python3 get-pip.py
!myenv/bin/pip install numpy
!myenv/bin/pip listThe deeper lesson wasn’t really about Colab quirks — it was that a virtual environment exists specifically to keep one project’s installed packages separate from another’s, so two projects needing different versions of the same library don’t collide. Colab already runs each notebook in its own isolated container, which is exactly why creating a second isolated environment inside it behaves a little differently than on a normal machine.
Module vs. package, and other things I had to actually look up
A few conceptual questions from this set were less about writing code and more about vocabulary I’d been using loosely:
- A module is a single
.pyfile; a package is a folder of modules bundled together (numpyis a package,mathis a module). import mathrequires prefixing everything (math.sqrt(16));from math import sqrtlets you drop the prefix (sqrt(16)) but risks name clashes if two modules define the same name.pip install package==1.2.3installs an exact version, which matters more than it sounds once you're trying to reproduce someone else's environment.
None of these are complicated once you say them out loud, but I’d been treating “module” and “package” as interchangeable for weeks before this exercise forced me to actually check.
What I’d tell someone hitting these same errors
Don’t skip past the SyntaxError or the No such file or directory message assuming it means you did the task wrong. More often than not with this batch, the concept was fine and the environment was the problem — Jupyter running shell commands as Python, or Colab handling venvs differently than a laptop would. Read the actual error text before assuming you misunderstood the exercise.
The other thing: math, datetime, and json all feel like small, almost boring modules to practice on their own. But date math, JSON files, and safe error handling are exactly what shows up once you're reading a real dataset or building anything that takes user input seriously — this is the unglamorous plumbing that everything else sits on top of.





