Python · Beginner

Python basics for beginners on a real dataset

10 min read

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

ideaexampleuse it for
values: strings, numbers, booleans, None"Adelie", 3750.0, True, Nonesingle 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
loopfor row in rows:doing the same thing for each item
functiondef 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:

python
print(max(["3750", "475", "1000"]))
text
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:

text
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 at 0, 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?":

python
row = {"species": "Adelie"}
print(type(row), type(row["species"]))
print([name for name in dir(row) if not name.startswith("_")])
help(dict.get)
text
<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:

text
  File "/home/asha/penguins/noindent.py", line 2
    print(i)
    ^^^^^
IndentationError: expected an indented block after 'for' statement on line 1

Mixing a tab and spaces in the same block (common after pasting code from a web page):

text
  File "/home/asha/penguins/tabs.py", line 3
    	y = 2
    ^
TabError: inconsistent use of tabs and spaces in indentation

TabError 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:

specmeaningexample output
{species:<10}left-align in 10 charactersAdelie
{n:>5}right-align in 5 152
{mean:>8.1f}right-align in 8, one decimal 3700.7
{mass:.0f}no decimals2850

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:

text
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,2007

Step 1: look before you calculate

explore.py
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])
text
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

penguins_report.py
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}")
text
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   6300

What 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

islands.py
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}")
text
Biscoe     Adelie       44
Biscoe     Gentoo      124
Dream      Adelie       56
Dream      Chinstrap    68
Torgersen  Adelie       52

Gentoo 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:

gotchas.py
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"))
text
['Adelie']
['Adelie', 'Gentoo']
['Adelie']
['Gentoo']

Use None as the default and create the list inside the function.

Comparing floats with ==

python
import math
print(0.1 + 0.2 == 0.3, 0.1 + 0.2)
print(math.isclose(0.1 + 0.2, 0.3))
text
False 0.30000000000000004
True

Floats 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", and max() on strings compares letters. Convert with a function like to_float first.

  • Calling float() on every value without checking for missing markers. One NA stops 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 KeyError on the first new key. Use defaultdict(int) (or Counter, covered in the dictionaries lesson).

  • Mutable default arguments. def f(x, items=[]) shares one list across all calls. Default to None.

  • Comparing floats with ==. Use math.isclose.

  • Forgetting newline="" and encoding="utf-8" when opening CSV files. Without them you get blank lines on Windows or a UnicodeDecodeError on 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."}

python
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.

python
def duplicates(items):
    seen, dupes = set(), set()
    for item in items:
        if item in seen:
            dupes.add(item)
        seen.add(item)
    return dupes

O(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.

  1. 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.

  2. Find the heaviest penguin of each sex (skip rows where sex is NA; there are 11) and print its species, island and mass.

  3. 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"}

practice.py
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")
text
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 g

Two 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