Python · Beginner
Python basics for beginners on a real dataset
Why it matters
Most "Python basics for beginners" material teaches each idea on toy values like x = 5. Real work is different: you open a file someone else made, every value arrives as text, some values are missing, and you have to group and summarise before anyone will look at your result. Python appears in 60% of the Data Analyst and 84% of the Data Scientist job descriptions we read, and online screening tests for those roles usually include one or two plain Python questions before pandas is allowed.
This lesson teaches the core building blocks together, on one real file, the way you will use them at work.
The concept
Five building blocks
| idea | example | use it for |
|---|---|---|
values: strings, numbers, booleans, None | "Adelie", 3750.0, True, None | single facts; None means "no value" |
| list | [3750.0, 3800.0, 3250.0] | an ordered collection you add to and loop over |
| dictionary | {"species": "Adelie", "body_mass_g": "3750"} | named fields, lookup by key |
| loop | for row in rows: | doing the same thing for each item |
| function | def mean(values): ... | giving a piece of logic a name so you can reuse and test it |
A CSV file becomes a list of dictionaries: one dictionary per row, keyed by the column names. Almost every beginner data task is a loop over that list that fills another dictionary.
Everything from a CSV is a string
csv.DictReader does not guess types. "3750" is text, so "3750" + "100" gives "3750100", and comparing strings compares them letter by letter:
print(max(["3750", "475", "1000"]))475"4" sorts after "3" and "1", so the string "475" "wins". Convert numbers yourself, on purpose, before you do maths or comparisons.
Missing values must be handled explicitly
The penguins file writes missing values as NA. Converting that directly crashes:
Traceback (most recent call last):
File "<string>", line 2, in <module>
float("NA")
~~~~~^^^^^^
ValueError: could not convert string to float: 'NA'The fix is a small function that turns every "missing" marker into None, and then code that checks if mass is None before using the value. Never replace missing values with 0: a penguin with no recorded weight does not weigh zero grams, and zeros would drag every average down.
Grouping with defaultdict
A plain dictionary raises KeyError the first time you do counts["Adelie"] += 1, because the key does not exist yet. collections.defaultdict creates the missing value for you:
defaultdict(int)starts every new key at0, perfect for counting.defaultdict(list)starts every new key at[], perfect for collecting values per group.
A tuple such as (island, species) can be a key too, which gives you two-level grouping without nested dictionaries.
Inspecting things you do not recognise
Three built-ins answer "what is this and what can I do with it?":
row = {"species": "Adelie"}
print(type(row), type(row["species"]))
print([name for name in dir(row) if not name.startswith("_")])
help(dict.get)<class 'dict'> <class 'str'>
['clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']
Help on method_descriptor:
get(self, key, default=None, /) unbound builtins.dict method
Return the value for key if key is in the dictionary, else default.type() tells you what it is, dir() lists what it can do (skip the _ names for now), and help() explains one thing. They work on every object, including ones from libraries you have never seen.
Indentation is syntax
Python uses indentation, not braces, to decide which lines belong to a loop, an if or a function. Use four spaces per level (VS Code does this for you). Forgetting to indent:
File "/home/asha/penguins/noindent.py", line 2
print(i)
^^^^^
IndentationError: expected an indented block after 'for' statement on line 1Mixing a tab and spaces in the same block (common after pasting code from a web page):
File "/home/asha/penguins/tabs.py", line 3
y = 2
^
TabError: inconsistent use of tabs and spaces in indentationTabError is a kind of IndentationError. Fix it by re-indenting the block with spaces; in VS Code, run "Convert Indentation to Spaces" from the command palette.
f-strings for readable output
An f-string puts values inside text: f"{species} weighs {mass} g". After a colon you can add a format spec:
| spec | meaning | example output |
|---|---|---|
{species:<10} | left-align in 10 characters | Adelie |
{n:>5} | right-align in 5 | 152 |
{mean:>8.1f} | right-align in 8, one decimal | 3700.7 |
{mass:.0f} | no decimals | 2850 |
Right-align numbers and left-align text, and a list of results turns into a table people can read.
Worked example: species report for 344 penguins
The Palmer Penguins dataset (CC0) records 344 penguins of three species from three islands near Palmer Station, Antarctica, between 2007 and 2009. Download penguins.csv into a folder called penguins. The first lines look like this:
species,island,bill_length_mm,bill_depth_mm,flipper_length_mm,body_mass_g,sex,year
Adelie,Torgersen,39.1,18.7,181,3750,male,2007
Adelie,Torgersen,39.5,17.4,186,3800,female,2007
Adelie,Torgersen,40.3,18,195,3250,female,2007Step 1: look before you calculate
import csv
with open("penguins.csv", newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
print(len(rows), "rows")
print(rows[0])
print(type(rows[0]["body_mass_g"]), repr(rows[0]["body_mass_g"]))
print(rows[3])344 rows
{'species': 'Adelie', 'island': 'Torgersen', 'bill_length_mm': '39.1', 'bill_depth_mm': '18.7', 'flipper_length_mm': '181', 'body_mass_g': '3750', 'sex': 'male', 'year': '2007'}
<class 'str'> '3750'
{'species': 'Adelie', 'island': 'Torgersen', 'bill_length_mm': 'NA', 'bill_depth_mm': 'NA', 'flipper_length_mm': 'NA', 'body_mass_g': 'NA', 'sex': 'NA', 'year': '2007'}Three facts before writing any analysis: there are 344 rows, every value is a string (even the year), and row 4 has no measurements at all. repr() shows the quotes, which is how you spot "a number that is actually text".
Step 2: the report
import csv
from collections import defaultdict
def to_float(text):
"""Return text as a float, or None for the 'NA' and blanks the file uses for missing values."""
if text in ("", "NA"):
return None
return float(text)
def mean(values):
"""Average of a non-empty list of numbers."""
return sum(values) / len(values)
with open("penguins.csv", newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
counts = defaultdict(int) # species -> number of penguins
masses = defaultdict(list) # species -> list of body masses
missing = []
for row in rows:
species = row["species"]
counts[species] += 1
mass = to_float(row["body_mass_g"])
if mass is None:
missing.append((species, row["island"], row["year"]))
else:
masses[species].append(mass)
print(f"{len(rows)} penguins, {len(missing)} without a body mass: {missing}")
print()
print(f"{'species':<10} {'count':>5} {'weighed':>7} {'mean g':>8} {'min g':>6} {'max g':>6}")
for species in sorted(counts):
values = masses[species]
print(f"{species:<10} {counts[species]:>5} {len(values):>7} {mean(values):>8.1f} "
f"{min(values):>6.0f} {max(values):>6.0f}")344 penguins, 2 without a body mass: [('Adelie', 'Torgersen', '2007'), ('Gentoo', 'Biscoe', '2009')]
species count weighed mean g min g max g
Adelie 152 151 3700.7 2850 4775
Chinstrap 68 68 3733.1 2700 4800
Gentoo 124 123 5076.0 3950 6300What the output tells you:
Count and weighed differ for Adelie and Gentoo. Reporting both is honest: anyone checking your average can see it is over 151 penguins, not 152.
Gentoo penguins are about 1.4 kg heavier on average than the other two species, which are almost the same as each other. That is a real finding you can now put in a sentence.
The missing rows are listed, with enough context (species, island, year) for someone to go and check the source.
Step 3: two-level grouping with tuple keys
import csv
from collections import defaultdict
with open("penguins.csv", newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
by_island = defaultdict(int)
for row in rows:
by_island[(row["island"], row["species"])] += 1
for (island, species), n in sorted(by_island.items()):
print(f"{island:<10} {species:<10} {n:>4}")Biscoe Adelie 44
Biscoe Gentoo 124
Dream Adelie 56
Dream Chinstrap 68
Torgersen Adelie 52Gentoo live only on Biscoe and Chinstrap only on Dream in this sample, while Adelie appear on all three islands. So if you compare islands by body mass, you are mostly comparing species. Spotting that kind of hidden grouping is the analyst's job, and a five-line loop found it.
Two gotchas every interviewer knows
Mutable default arguments
Default values are created once, when Python reads the def line, not each time the function runs:
def add_sighting(name, log=[]):
log.append(name)
return log
print(add_sighting("Adelie"))
print(add_sighting("Gentoo")) # surprise: the list from the first call is back
def add_sighting_fixed(name, log=None):
if log is None:
log = []
log.append(name)
return log
print(add_sighting_fixed("Adelie"))
print(add_sighting_fixed("Gentoo"))['Adelie']
['Adelie', 'Gentoo']
['Adelie']
['Gentoo']Use None as the default and create the list inside the function.
Comparing floats with ==
import math
print(0.1 + 0.2 == 0.3, 0.1 + 0.2)
print(math.isclose(0.1 + 0.2, 0.3))False 0.30000000000000004
TrueFloats are stored in binary and most decimal fractions cannot be represented exactly. Compare with math.isclose, and for money use decimal.Decimal (the strings and numbers lesson covers both).
Common mistakes
Doing maths on strings.
"3750" + "100"is"3750100", andmax()on strings compares letters. Convert with a function liketo_floatfirst.Calling
float()on every value without checking for missing markers. OneNAstops the whole script. Decide what "missing" looks like in this file (NA, empty,-,null) and handle it in one place.Replacing missing values with 0. It silently changes every average. Skip them and report how many you skipped.
Using a plain dict for counting and getting
KeyErroron the first new key. Usedefaultdict(int)(orCounter, covered in the dictionaries lesson).Mutable default arguments.
def f(x, items=[])shares one list across all calls. Default toNone.Comparing floats with
==. Usemath.isclose.Forgetting
newline=""andencoding="utf-8"when opening CSV files. Without them you get blank lines on Windows or aUnicodeDecodeErroron non-English text.
Interview angle
Screening rounds for analyst and data roles love small tasks on lists and dictionaries. They all have the same shape: loop once, fill a dictionary, read the answer from it.
:::toggle{title="Count how often each word appears in a sentence."}
from collections import defaultdict
def word_counts(text):
counts = defaultdict(int)
for word in text.lower().split():
counts[word] += 1
return dict(counts)Say the complexity out loud: one pass over n words, and each dictionary update is O(1) on average, so O(n) overall. :::
:::toggle{title="Find the duplicates in a list."} Keep a set of what you have seen; anything already in it is a duplicate.
def duplicates(items):
seen, dupes = set(), set()
for item in items:
if item in seen:
dupes.add(item)
seen.add(item)
return dupesO(n) because set membership is O(1) on average. The nested-loop version (compare every pair) is O(n²) and is what the interviewer hopes you avoid. :::
:::toggle{title="What is wrong with def add(item, bucket=[])?"} The default list is created once when the function is defined and reused on every call, so items from earlier calls leak into later ones. Use bucket=None and create a new list inside the function. :::
Before you type, explain your approach in one sentence and name the data structure. Interviewers grade the reasoning as much as the code.
Practice task
Use penguins.csv and only the standard library.
Write
median(values)without any library (sort, then take the middle value, or the average of the two middle values for an even count) and print the median body mass per species.Find the heaviest penguin of each sex (skip rows where
sexisNA; there are 11) and print its species, island and mass.Print a table of mean flipper length per island, aligned with f-strings, with a column showing how many penguins had a flipper measurement.
Done when: your medians are Adelie 3700.0, Chinstrap 3700.0 and Gentoo 5000.0, and your script runs without errors on the full file.
:::toggle{title="Show a solution for steps 1 and 2"}
import csv
from collections import defaultdict
def to_float(text):
return None if text in ("", "NA") else float(text)
def median(values):
ordered = sorted(values)
n = len(ordered)
middle = n // 2
if n % 2 == 1:
return ordered[middle]
return (ordered[middle - 1] + ordered[middle]) / 2
with open("penguins.csv", newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
masses = defaultdict(list)
heaviest = {} # sex -> row of the heaviest penguin seen so far
for row in rows:
mass = to_float(row["body_mass_g"])
if mass is None:
continue
masses[row["species"]].append(mass)
sex = row["sex"]
if sex == "NA":
continue
if sex not in heaviest or mass > to_float(heaviest[sex]["body_mass_g"]):
heaviest[sex] = row
for species in sorted(masses):
print(f"{species:<10} median {median(masses[species]):>7.1f} g")
for sex, row in sorted(heaviest.items()):
print(f"heaviest {sex:<6}: {row['species']} on {row['island']}, {row['body_mass_g']} g")Adelie median 3700.0 g
Chinstrap median 3700.0 g
Gentoo median 5000.0 g
heaviest female: Gentoo on Biscoe, 5200 g
heaviest male : Gentoo on Biscoe, 6300 gTwo females weigh 5200 g. Because the comparison is a strict >, the first one in the file is kept. Mention ties like this in your answer: interviewers notice. :::
Further reading
Palmer Penguins: the dataset, its licence (CC0) and the paper it comes from.
The Python tutorial: the official tour; chapters 3 to 5 match this lesson.
csv module:
DictReader, dialects and thenewline=""rule.collections.defaultdict: how the default factory works.