Already know Excel? You're closer to Python than you think. This hands-on lesson translates your spreadsheet intuition into Python fundamentals — variables, lists, dictionaries, and loops — using realistic data scenarios you'll actually encounter on the job.

If you've spent years working in Excel, you already think like a programmer — you just don't know it yet. When you write a VLOOKUP, you're mapping one value to another. When you drag a formula down a column, you're looping over rows. When you use named ranges, you're naming data so you can reference it later. Python doesn't change what you're trying to do with data; it changes how you do it — and for the kinds of tasks data professionals deal with daily, Python does it faster, more reliably, and at a scale that would crash Excel.
This lesson bridges the gap between your Excel intuition and Python fundamentals. We're not going to start with abstract computer science concepts. We're going to start with problems you've already solved in Excel and show you how to solve them in Python — and then push past what Excel can do. By the end of this lesson, you'll have a working mental model for Python's four most essential building blocks: variables, lists, dictionaries, and loops.
This is the lesson I wish I'd had when I made the switch. Let's get into it.
What you'll learn:
for loops and while loops to automate repetitive tasksYou should be comfortable opening and running Python in a development environment. If you haven't set that up yet, start with Setting Up Python for Data Analysis: Python, VS Code, Jupyter, and Virtual Environments before continuing here. All code examples in this lesson run in either a Jupyter notebook or a standard Python script — either works fine.
You don't need to know Python already, but you should be comfortable with Excel at an intermediate level: formulas, named ranges, and basic data manipulation.
In Excel, you can name a cell. You click on cell B2, go to the Name Box, type tax_rate, and now you can reference that cell by name in formulas. Python's variables work on the same idea, but they're far more powerful.
A Python variable is a name that points to a value stored in memory. The value can be a number, text, a list of thousands of items, a table of data, or almost anything else. You create one with a simple assignment:
tax_rate = 0.08
region = "Northeast"
total_sales = 142500.75
is_q4 = True
No declaration, no data type boxes to fill out. Python figures out what kind of data you're storing automatically. This is called dynamic typing, and it's one of the reasons Python code reads so naturally.
Excel has a loose type system — a cell is either a number, text, or a date, roughly. Python is more explicit, and understanding these types will save you from a class of frustrating bugs.
Integers (int): Whole numbers. Row counts, product IDs, years.
row_count = 52483
fiscal_year = 2024
Floats (float): Decimal numbers. Prices, percentages, averages.
avg_order_value = 87.42
margin_pct = 0.213
Strings (str): Text. Always wrapped in quotes — single or double, Python accepts both.
store_name = "Chicago Flagship"
product_sku = 'WD-4421-BLK'
Booleans (bool): True or False. Useful for flags and conditions.
is_active = True
has_returned = False
Tip
In Excel, if you type 08 into a cell formatted as a number, it becomes 8. In Python, writing 08 as an integer actually throws a syntax error. Leading zeros in numeric literals aren't allowed. If you need to preserve leading zeros — say, for ZIP codes — store them as strings: zip_code = "08540".
One of the most common early frustrations: data coming from CSV files or databases arrives as strings even when it looks like numbers. You'll see this constantly when you start loading real data.
revenue_str = "142500" # This came in from a CSV as text
revenue_int = int(revenue_str) # Convert to integer
revenue_float = float(revenue_str) # Convert to float
# Now you can do math on it
print(revenue_float * 1.08) # 153900.0
The reverse happens too. You might need to build a string that includes a number:
month = 3
year = 2024
label = "Report_" + str(month) + "_" + str(year)
print(label) # Report_3_2024
That str() call is necessary — Python won't silently add a number to a string the way Excel sometimes does. It'll throw a TypeError instead. Which, honestly, is better behavior: you catch the problem immediately instead of discovering a corrupted output three steps later.
Python variable names must start with a letter or underscore, can contain letters, numbers, and underscores, and are case-sensitive. Beyond the hard rules, the community convention is snake_case — all lowercase with underscores between words.
# Good
monthly_revenue = 45200
customer_count = 1842
# Works but avoid it
MonthlyRevenue = 45200 # This is "PascalCase" — Python uses it for class names
MONTHLY_REVENUE = 45200 # ALL_CAPS is convention for constants
# Will break your code
2024_revenue = 45200 # Can't start with a number — SyntaxError
monthly-revenue = 45200 # Hyphens not allowed — Python reads this as subtraction
In Excel, a column is your natural container for a sequence of values. In Python, that's a list. A list is an ordered, mutable (changeable) collection of items. The items can be any type — and they can even be mixed types, though you'll usually keep them consistent.
# A column of region names
regions = ["Northeast", "Southeast", "Midwest", "Southwest", "West"]
# A column of monthly revenue figures
monthly_revenue = [45200, 52100, 48900, 61300, 55800, 49200]
# Index starts at 0, not 1 — this is the #1 adjustment for Excel users
print(regions[0]) # Northeast
print(regions[4]) # West (the 5th item)
print(regions[-1]) # West — negative indexing counts from the end
Warning
Python's list index starts at 0, not 1. After years of Excel's row 1 being the first row, this trips everyone up. When you access regions[0], you get the first item. When you access regions[5] on a five-item list, you get an IndexError. Burn this into your memory early.
In Excel, you select a range like B2:B10. In Python, you slice a list:
monthly_revenue = [45200, 52100, 48900, 61300, 55800, 49200, 58700, 63100, 57400, 52800, 61900, 68200]
# First quarter (months 0, 1, 2)
q1 = monthly_revenue[0:3]
print(q1) # [45200, 52100, 48900]
# Last quarter
q4 = monthly_revenue[9:12]
print(q4) # [52800, 61900, 68200]
# Every other month
every_other = monthly_revenue[::2]
print(every_other) # [45200, 48900, 55800, 58700, 57400, 61900]
The slice notation [start:stop:step] works like this: start is included, stop is excluded. So [0:3] gives you indexes 0, 1, and 2 — three items.
Unlike a named range in Excel, a Python list is easily modified in code:
products = ["Widget A", "Widget B", "Widget C"]
# Add an item to the end
products.append("Widget D")
print(products) # ['Widget A', 'Widget B', 'Widget C', 'Widget D']
# Insert at a specific position
products.insert(1, "Widget A+")
print(products) # ['Widget A', 'Widget A+', 'Widget B', 'Widget C', 'Widget D']
# Remove by value
products.remove("Widget A+")
print(products) # ['Widget A', 'Widget B', 'Widget C', 'Widget D']
# Update a value by index
products[0] = "Widget Alpha"
print(products) # ['Widget Alpha', 'Widget B', 'Widget C', 'Widget D']
# How many items?
print(len(products)) # 4
scores = [88, 92, 75, 95, 81, 67, 90, 73, 88, 94]
print(sum(scores)) # 843 — like SUM() in Excel
print(max(scores)) # 95 — like MAX() in Excel
print(min(scores)) # 67 — like MIN() in Excel
print(len(scores)) # 10 — like COUNT() in Excel
print(sum(scores) / len(scores)) # 84.3 — manual AVERAGE()
# Sort the list
scores.sort()
print(scores) # [67, 73, 75, 81, 88, 88, 90, 92, 94, 95]
# Sort descending
scores.sort(reverse=True)
print(scores) # [95, 94, 92, 90, 88, 88, 81, 75, 73, 67]
# Is a value in the list?
print(88 in scores) # True
print(100 in scores) # False
Key insight
scores.sort() modifies the list in place — the original list is permanently reordered. If you want to keep the original and get a sorted copy, use sorted(scores) instead, which returns a new list without changing the original. This distinction between "mutating" and "non-mutating" operations will come up constantly in Python data work.
If lists are columns, dictionaries are two-column lookup tables. A dictionary stores data as key-value pairs — you look up a key, you get its value. Sound familiar? That's exactly what VLOOKUP does.
# A dictionary of region codes to region names — like a lookup table
region_lookup = {
"NE": "Northeast",
"SE": "Southeast",
"MW": "Midwest",
"SW": "Southwest",
"WE": "West"
}
# Look up a value by key — no VLOOKUP needed
print(region_lookup["NE"]) # Northeast
print(region_lookup["MW"]) # Midwest
The syntax: curly braces {} wrap the whole thing, colons : separate each key from its value, and commas separate each pair.
VLOOKUP fails with a #N/A error when a value isn't found. Python dictionaries give you better options:
region_lookup = {
"NE": "Northeast",
"SE": "Southeast",
"MW": "Midwest",
}
# This will raise a KeyError if the key doesn't exist
# region_lookup["WE"] # KeyError: 'WE'
# Safe lookup with .get() — returns None if not found
print(region_lookup.get("WE")) # None
print(region_lookup.get("WE", "Unknown Region")) # Unknown Region
The .get() method with a default value is the clean, safe way to do lookups. Think of it as IFERROR(VLOOKUP(...), "Unknown Region") — but less ugly.
You won't always type dictionaries out by hand. Often you'll build them from data:
# Two parallel lists — like two columns in a spreadsheet
store_ids = ["S001", "S002", "S003", "S004"]
store_names = ["Chicago Flagship", "NYC Midtown", "LA Westside", "Houston Downtown"]
# Build a lookup dictionary from both lists
store_lookup = {}
for i in range(len(store_ids)):
store_lookup[store_ids[i]] = store_names[i]
print(store_lookup)
# {'S001': 'Chicago Flagship', 'S002': 'NYC Midtown', 'S003': 'LA Westside', 'S004': 'Houston Downtown'}
# Or use the more Pythonic zip() approach
store_lookup = dict(zip(store_ids, store_names))
print(store_lookup) # Same result, cleaner code
Real lookup tables often have more than two columns. Dictionaries can be nested:
stores = {
"S001": {
"name": "Chicago Flagship",
"region": "Midwest",
"open_year": 2015,
"is_active": True
},
"S002": {
"name": "NYC Midtown",
"region": "Northeast",
"open_year": 2018,
"is_active": True
},
"S003": {
"name": "LA Westside",
"region": "West",
"open_year": 2020,
"is_active": False
}
}
# Access nested values
print(stores["S001"]["name"]) # Chicago Flagship
print(stores["S002"]["region"]) # Northeast
print(stores["S003"]["is_active"]) # False
This is the Python equivalent of a proper reference table — and once you start working with JSON data from APIs, you'll see this structure constantly.
region_lookup = {
"NE": "Northeast",
"SE": "Southeast",
"MW": "Midwest",
}
# Loop over keys
for code in region_lookup:
print(code) # NE, SE, MW
# Loop over keys AND values
for code, name in region_lookup.items():
print(f"{code} → {name}")
# NE → Northeast
# SE → Southeast
# MW → Midwest
# Just the keys
print(list(region_lookup.keys())) # ['NE', 'SE', 'MW']
# Just the values
print(list(region_lookup.values())) # ['Northeast', 'Southeast', 'Midwest']
In Excel, when you want to apply a formula to 10,000 rows, you drag it down. Or you use Ctrl+D. Or you copy and paste. It works, but it's manual and fragile. In Python, loops automate that repetition precisely.
A for loop iterates over a sequence — a list, the keys of a dictionary, a range of numbers — and executes a block of code for each item:
monthly_revenue = [45200, 52100, 48900, 61300, 55800, 49200]
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
for i in range(len(monthly_revenue)):
print(f"{months[i]}: ${monthly_revenue[i]:,}")
Output:
Jan: $45,200
Feb: $52,100
Mar: $48,900
Apr: $61,300
May: $55,800
Jun: $49,200
Tip
That f"..." syntax is an f-string — Python's cleanest way to embed variables in strings. The {monthly_revenue[i]:,} part uses Python's format spec language: the :, adds comma separators to the number. F-strings replace the old .format() method and are now the standard approach.
Reaching for range(len(...)) is common but slightly clunky. enumerate() is cleaner when you need both the index and the value:
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
monthly_revenue = [45200, 52100, 48900, 61300, 55800, 49200]
for index, month in enumerate(months):
print(f"Month {index + 1} ({month}): ${monthly_revenue[index]:,}")
Every useful loop includes some logic. Python's if/elif/else works like Excel's IF() function — with cleaner syntax:
monthly_revenue = [45200, 52100, 48900, 61300, 55800, 49200]
target = 50000
for i, revenue in enumerate(monthly_revenue):
if revenue >= target:
status = "✓ Met"
else:
status = "✗ Missed"
print(f"Month {i+1}: ${revenue:,} — {status}")
Output:
Month 1: $45,200 — ✗ Missed
Month 2: $52,100 — ✓ Met
Month 3: $48,900 — ✗ Missed
Month 4: $61,300 — ✓ Met
Month 5: $55,800 — ✓ Met
Month 6: $49,200 — ✗ Missed
One of the most common patterns: loop over data, calculate something, collect the results into a new list.
prices = [12.99, 45.00, 7.49, 89.95, 23.50]
tax_rate = 0.08
prices_with_tax = [] # Start with an empty list
for price in prices:
total = price * (1 + tax_rate)
prices_with_tax.append(round(total, 2))
print(prices_with_tax)
# [14.03, 48.6, 8.09, 97.15, 25.38]
This is the Python equivalent of writing a formula in column B that references column A, then filling it down for every row.
Once you're comfortable with the basic loop pattern above, there's a more concise version called a list comprehension. It's worth knowing because you'll see it everywhere in Python code:
prices = [12.99, 45.00, 7.49, 89.95, 23.50]
tax_rate = 0.08
# Same as the loop above, in one line
prices_with_tax = [round(price * (1 + tax_rate), 2) for price in prices]
print(prices_with_tax)
# [14.03, 48.6, 8.09, 97.15, 25.38]
You can also add conditions:
# Only include prices over $20
high_value_prices = [price for price in prices if price > 20]
print(high_value_prices) # [45.0, 89.95, 23.5]
Note
List comprehensions are fast and idiomatic Python, but don't over-use them. If the logic inside is complex (multiple conditions, nested operations), a regular for loop is more readable. Write for clarity first, brevity second.
The for loop is right when you're working through a known sequence. The while loop runs as long as a condition is true — useful when you don't know ahead of time how many iterations you'll need.
# Simulate a running total until we hit a revenue target
monthly_revenues = [45200, 52100, 48900, 61300, 55800, 49200, 58700]
target = 200000
cumulative = 0
month = 0
while cumulative < target:
cumulative += monthly_revenues[month]
month += 1
print(f"After month {month}: ${cumulative:,}")
print(f"\nReached target after {month} months.")
Output:
After month 1: $45,200
After month 2: $97,300
After month 3: $146,200
After month 4: $207,500
Reached target after 4 months.
Warning
A while loop with a condition that never becomes False will run forever — an infinite loop. Always make sure something inside the loop changes the condition. If your script seems frozen, press Ctrl+C in the terminal (or interrupt the kernel in Jupyter) to stop it.
Let's combine everything we've covered to solve a realistic problem: you have sales transaction data (represented here as a list of dictionaries), and you want to calculate total revenue by region and flag underperforming regions.
This is the kind of analysis you'd normally do with a pivot table or SUMIF in Excel. In Python, you do it with a loop and a dictionary.
# Simulated transaction data — like rows in a spreadsheet
transactions = [
{"id": "T001", "region": "Northeast", "amount": 2400.00, "product": "Widget A"},
{"id": "T002", "region": "Midwest", "amount": 870.50, "product": "Widget B"},
{"id": "T003", "region": "Northeast", "amount": 1150.00, "product": "Widget C"},
{"id": "T004", "region": "West", "amount": 3200.00, "product": "Widget A"},
{"id": "T005", "region": "Southeast", "amount": 945.00, "product": "Widget B"},
{"id": "T006", "region": "Midwest", "amount": 2100.00, "product": "Widget A"},
{"id": "T007", "region": "West", "amount": 1800.00, "product": "Widget C"},
{"id": "T008", "region": "Northeast", "amount": 3500.00, "product": "Widget A"},
{"id": "T009", "region": "Southeast", "amount": 620.00, "product": "Widget B"},
{"id": "T010", "region": "Midwest", "amount": 1350.00, "product": "Widget C"},
]
# Step 1: Build a revenue-by-region summary (like a SUMIF or pivot table)
revenue_by_region = {}
for transaction in transactions:
region = transaction["region"]
amount = transaction["amount"]
if region in revenue_by_region:
revenue_by_region[region] += amount
else:
revenue_by_region[region] = amount
print("Revenue by Region:")
print("-" * 30)
for region, total in revenue_by_region.items():
print(f" {region:<12} ${total:>10,.2f}")
# Step 2: Calculate total and average across regions
total_revenue = sum(revenue_by_region.values())
avg_revenue = total_revenue / len(revenue_by_region)
print(f"\nTotal Revenue: ${total_revenue:,.2f}")
print(f"Average by Region: ${avg_revenue:,.2f}")
# Step 3: Flag underperforming regions (below average)
threshold = avg_revenue * 0.80 # More than 20% below average
print("\nPerformance Flags:")
print("-" * 30)
for region, total in revenue_by_region.items():
if total < threshold:
flag = "⚠ Below threshold"
elif total > avg_revenue * 1.20:
flag = "★ Strong"
else:
flag = " On target"
print(f" {region:<12} {flag}")
Output:
Revenue by Region:
------------------------------
Northeast $ 7,050.00
Midwest $ 4,320.50
West $ 5,000.00
Southeast $ 1,565.00
Total Revenue: $17,935.50
Average by Region: $4,483.88
Performance Flags:
------------------------------
Northeast ★ Strong
Midwest On target
West On target
Southeast ⚠ Below threshold
This is roughly a hundred lines of Excel work — building a helper table, writing SUMIFs, building a performance flag column with nested IFs — reduced to clean, readable Python that runs in milliseconds on millions of rows.
Work through this on your own before looking at any hints. Write Python code to solve the following:
Scenario: You have a list of employee records. Each record is a dictionary with keys name, department, and salary. Your tasks:
Stretch goal: Count how many employees are in each department and calculate the average salary per department (not total — per person).
This exercise covers every concept in this lesson. If you get stuck, trace back through the sections above — everything you need is there.
Off-by-one errors with list indexes
Accessing my_list[len(my_list)] is always an IndexError. The last valid index is len(my_list) - 1. When slicing, remember the stop index is exclusive: my_list[0:3] gives you items at indexes 0, 1, 2 — not 3.
Modifying a list while iterating over it This one causes subtle, hard-to-find bugs:
# BAD — unpredictable behavior
items = [1, 2, 3, 4, 5]
for item in items:
if item % 2 == 0:
items.remove(item) # Don't do this
# GOOD — filter to a new list
items = [item for item in items if item % 2 != 0]
KeyError when accessing a dictionary
If you write my_dict["key"] and the key doesn't exist, you get a KeyError. Use .get() with a default when the key might be absent: my_dict.get("key", default_value).
Strings that look like numbers
value = "42"
print(value + 8) # TypeError: can only concatenate str (not "int") to str
print(int(value) + 8) # 50 — correct
This is especially common when reading data from CSV files. Always check your types with type(variable) when something isn't adding up.
Mutable default trap with .sort()
original = [5, 3, 1, 4, 2]
original.sort() # Modifies original in place — original is now [1, 2, 3, 4, 5]
# If you needed the original order later, it's gone
# Use sorted() to preserve the original:
original = [5, 3, 1, 4, 2]
sorted_copy = sorted(original) # original unchanged
Forgetting the colon after for, if, while
Python uses indentation and colons to define code blocks. Missing the colon is a SyntaxError. Your IDE will typically flag it immediately, but it's the kind of thing that trips you up at 11pm.
for item in my_list # SyntaxError: expected ':'
for item in my_list: # Correct
You now have a working command of Python's four foundational building blocks:
sum(), min(), and max()for or running until a condition changes with whileMore importantly, you now understand how these tools connect: loops iterate over lists, loops build dictionaries, dictionaries contain lists as values, lists of dictionaries represent tabular data. That interconnection is the foundation of almost everything in data analysis with Python.
What you've built in this lesson manually — the grouping, the aggregation, the flagging — pandas (the core data analysis library) handles automatically with DataFrames. The reason we did it by hand first is so you understand what pandas is doing for you when you write a one-line .groupby(). That mental model matters when things go wrong.
From here, your natural next step is getting pandas in hand and loading your first real dataset. The operations you performed in this lesson — grouping by category, summing values, comparing to averages — translate directly to pandas syntax. The concepts are identical; the syntax gets shorter and the performance gets dramatically better.
The path from Excel to production-quality Python data analysis is shorter than it looks. You've just covered more ground than most "introduction to Python" courses manage in their first three weeks.