Learn python, with practice after every lesson
10 lessons, about 151 minutes of reading, and 30 multiple-choice questions. Each lesson names the mistake that most often costs people the interview, because that is where the hours actually go. Part of the free coding course.
Variables and types
BasicsA variable is a name for a value you want to use again. Python does not ask you to declare the type β you assign a value and Python works out what it is. That convenience is why Python is usually the fastest first language for an adult learner, and it is also the first thing that will confuse you when a number arrives from a spreadsheet as text and refuses to add up.
Four types cover almost everything you will do early on: a whole number (int), a decimal (float), text (str), and a true/false value (bool). Knowing which you are holding matters, because Python will refuse to add text to a number rather than guess what you meant. That refusal is a feature. Languages that guess produce the bug where 5 + "3" quietly becomes 53 and nobody notices until a total is wrong.
The habit worth building now is checking rather than assuming. When something behaves strangely β a comparison that should be true isn't, a sum that comes out as a long string of digits β type(x) tells you what you are actually holding in one line. Most of the confusion in the first fortnight is a str pretending to be an int, and this is how you catch it in seconds instead of an hour.
Syntax
name = "Priya" # str
age = 29 # int
rate = 1250.50 # float
is_available = True # bool
print(f"{name} is {age} and charges {rate}/hr")
print(type(age)) # <class 'int'>
Key points
- Assignment uses a single =, comparison uses ==. Mixing them is the single most common first-week bug.
- type(x) tells you what you are actually holding when something behaves unexpectedly.
- f-strings (f"...{value}...") are the modern way to build text; older tutorials use + or %.
Practice challenge
Calculate average = (sum of marks) / (number of exams)
81.66666666666667
Show a hint
- Average = Sum / Count
- Add all three marks first
- Divide by 3
Check yourself
1. What does type(29) return?
Show answer
B. <class 'int'>
2. Which line correctly builds a message?
Show answer
A. f"Hi {name}"
3. Which is assignment, not comparison?
Show answer
B. age = 29
Lists and loops
BasicsA list holds several values in order. Most real work is a loop over a list: every row in a spreadsheet, every file in a folder, every customer in an export. Once you can loop over a list and do something to each item, you can automate most repetitive office work β and that is genuinely the point at which people stop describing themselves as learning to code.
Python's for loop reads almost like English: for each item in the list, do this. You rarely need to track an index manually, which is where people coming from other languages over-complicate things. If you find yourself writing for i in range(len(items)) and then using items[i], you are writing C in Python β iterate over the items directly and the code gets shorter and harder to get wrong.
The second habit is knowing what already exists. sum(), max(), min() and len() are built in, and a hand-written loop that adds numbers up is one of the clearest signals in a code review that someone is new. Reaching for the built-in is not laziness; it is correctness, because those functions handle the edge cases you have not thought about yet.
Syntax
scores = [82, 45, 91, 67]
total = 0
for s in scores:
total += s
print("Total:", total) # 285
print("Average:", total / len(scores))
print("Highest:", max(scores))
print("Passed:", [s for s in scores if s >= 60])
Key points
- len(list) gives the count; indexes start at 0, so the last item is list[len(list)-1] or simply list[-1].
- sum(), max(), min() already exist β writing a loop for these is a common beginner tell.
- A list comprehension [x for x in items if condition] filters in one readable line.
Practice challenge
Calculate mean = sum / count
80.0
Show a hint
- sum(marks) / len(marks)
- Sum = 400, Count = 5
- 400 / 5 = 80
Check yourself
1. What does scores[-1] give for [82, 45, 91]?
Show answer
C. 91
2. What is the index of the first item in a Python list?
Show answer
A. 0
3. Which filters a list to values over 60?
Show answer
A. [x for x in s if x > 60]
Functions and reuse
Working levelA function is a named block of code you can run repeatedly with different inputs. The moment you copy and paste a few lines and change one value, that is the signal to write a function instead. It is not about elegance β it is that the copied version now has two places to fix when the logic is wrong, and you will find one of them.
Good functions do one thing and return a value rather than printing it. Returning keeps the function useful in other contexts; printing locks it to the screen and makes it impossible to test. This distinction sounds academic until you try to write your first test and discover that the only way to check your function is to read the output with your own eyes, every time, forever.
Default arguments let one function serve several cases without duplication, and raising an error on impossible input is better than returning a wrong number quietly. A function that accepts a negative salary and returns a negative tax figure has not failed β it has produced a plausible answer that will travel a long way through a system before anyone questions it.
Syntax
def take_home(gross, tax_rate=0.2):
"""Return pay after tax. Rate defaults to 20%."""
if gross < 0:
raise ValueError("gross cannot be negative")
return round(gross * (1 - tax_rate), 2)
print(take_home(50000)) # 40000.0
print(take_home(50000, 0.3)) # 35000.0
Key points
- Default arguments (tax_rate=0.2) let one function serve several cases without duplication.
- return hands a value back; print only displays it. A function that prints cannot be reused or tested.
- Raise an error on impossible input rather than silently returning a wrong number.
Practice challenge
Calculate available bandwidth.
25
Show a hint
- Subtract current from max
- Available = max - current
- 100 - 75 = 25
Check yourself
1. What does take_home(50000) return with the default rate?
Show answer
B. 40000.0
2. Why prefer return over print inside a function?
Show answer
B. The value can be reused and tested
3. Which default argument is unsafe?
Show answer
C. x=[]
Dictionaries and real data
Working levelA dictionary maps a key to a value β a name to a phone number, an ID to a record. Almost all real data arriving from an API or a CSV becomes dictionaries, so this is the structure you will use most in working code, and being fluent with it is a large part of what separates someone who can follow a tutorial from someone who can handle a real file.
Lookup by key is effectively instant regardless of size, which is why a dictionary is the fix when you find yourself scanning a whole list repeatedly to find matching items. If you have a loop inside a loop comparing IDs, you almost certainly want a dictionary β the code gets shorter and the runtime drops from minutes to milliseconds on any real volume of data.
The habit that matters in production is never assuming a key exists. Real data has gaps: the customer with no email, the row where the region was left blank, the record that predates a field being added. employee["missing"] raises KeyError and stops the program; employee.get("missing", default) returns a fallback and keeps going. Choosing deliberately between those two is the actual skill.
Syntax
employee = {"name": "Ravi", "dept": "Finance", "years": 6}
print(employee["name"]) # Ravi
print(employee.get("city", "N/A")) # N/A β no KeyError
employee["city"] = "Pune" # add
for key, value in employee.items():
print(f"{key}: {value}")
Key points
- employee["missing"] raises KeyError; employee.get("missing", default) returns a fallback instead.
- .items() gives key and value together β the normal way to loop a dictionary.
- Keys must be unique; assigning an existing key overwrites rather than adding.
Practice challenge
Determine if battery needs charging.
False
Show a hint
- Compare voltage with minimum
- voltage < min means needs charging
- 11.2 >= 10.5 so OK
Check yourself
1. What does employee.get("city", "N/A") return when city is absent?
Show answer
C. "N/A"
2. Which loops keys and values together?
Show answer
A. .items()
3. Why use a dict instead of scanning a list repeatedly?
Show answer
B. Lookup by key is far faster
Making decisions with if
BasicsPrograms become useful the moment they can choose. An if statement runs a block only when a condition is true; elif checks another condition when the first fails; else catches everything remaining. Almost every piece of business logic you will ever write is a stack of these, describing rules somebody explained to you in a meeting.
Python uses indentation rather than braces to mark which lines belong to the block. This is not decoration β the indentation is the syntax, and getting it wrong changes what your program does rather than merely how it looks. A line indented one level too far runs inside the loop instead of after it, which is a bug that reads as correct until you check the output.
Order matters more than beginners expect. The first matching branch wins, so a condition like score >= 60 placed above score >= 80 means nobody ever gets the higher grade β the broader rule catches them first. When a set of rules produces one category too often, this ordering is almost always the reason, and it is worth checking before anything else.
Syntax
score = 72
if score >= 80:
grade = "A"
elif score >= 60:
grade = "B"
else:
grade = "C"
print(grade) # B
# Conditions can combine:
if score >= 60 and score < 80:
print("Solid pass")
Key points
- Order matters β the first matching branch wins, so put the narrowest condition first.
- and requires both sides true; or requires either; not flips the result.
- Comparison returns a real boolean, so `if is_active:` is cleaner than `if is_active == True:`.
Practice challenge
Print the result of comparing 85 >= 80.
True
Show a hint
- Use >= for comparison
- This creates a boolean value
- True or False are the results
Check yourself
1. With score = 72, which branch runs?
Show answer
B. B
2. What marks a block in Python?
Show answer
B. Indentation
3. Which is true only when both sides are true?
Show answer
B. and
Working with text
BasicsA great deal of office automation is text handling: cleaning a name, splitting a code, checking a file extension, stripping the stray spaces that arrive with every export anyone has ever sent you. Python's string methods cover almost all of it without any external library, which is why a short script can replace an afternoon of manual tidying.
Strings are immutable β every method returns a new string rather than changing the original. Beginners lose hours to this, calling s.upper() and wondering why s never changed. The rule is simple once you have been caught by it: if you want to keep the result, assign it back. s = s.upper(), not s.upper() on its own.
The methods worth knowing on day one are .strip() to remove surrounding whitespace, .split(sep) to break text into a list, and the in operator to test for a substring. Between them they handle most of the cleaning that real data needs, and chaining them β raw.strip().lower().replace(",", "") β reads left to right, each step feeding the next.
Syntax
raw = " Priya Sharma , Finance "
clean = raw.strip() # trim both ends
name, dept = clean.split(",")
print(name.strip().upper()) # PRIYA SHARMA
print(dept.strip().lower()) # finance
print("sharma" in clean.lower()) # True
print(clean.replace(",", " β"))
Key points
- .strip() removes surrounding whitespace; .split(sep) breaks text into a list.
- in tests for a substring β simpler than writing a search loop.
- Chain carefully: raw.strip().upper() reads left to right, each step feeding the next.
Practice challenge
Validate branch name format.
True
Show a hint
- Use .isalnum() or check characters
- Valid if only lowercase letters and hyphens
- No spaces or uppercase
Check yourself
1. What does " hi ".strip() return?
Show answer
B. "hi"
2. Does s.upper() change s itself?
Show answer
B. No β it returns a new string
3. How do you test whether text contains "abc"?
Show answer
A. "abc" in text
Errors and files
Working levelReal input breaks things: the file is missing, the number is text, the network drops halfway through. try/except lets you handle the failure deliberately instead of the program stopping dead with a stack trace that means nothing to the person who ran it. This is the difference between a script you can give someone and one only you can operate.
Catch the specific error you expect. A bare except swallows everything β including typos in your own code β and turns a five-second fix into an afternoon of confusion, because the program now fails silently in the wrong place with the wrong message. FileNotFoundError and ValueError are specific enough to tell you what actually went wrong.
The other half is closing what you opened. with open(...) closes the file automatically, even when an error is raised inside the block, which matters more than it sounds: files left open lock on Windows, exhaust handles on servers, and produce failures that appear hours later in something unrelated. Use with every time and the problem never exists.
Syntax
def read_total(path):
try:
with open(path) as f:
return sum(float(line) for line in f if line.strip())
except FileNotFoundError:
print(f"No file at {path}")
return 0
except ValueError:
print("File contains something that isn't a number")
return 0
print(read_total("sales.txt"))
Key points
- with open(...) closes the file automatically, even if an error is raised inside.
- Catch named exceptions (FileNotFoundError, ValueError) rather than a bare except.
- Returning a sensible default keeps the caller working; re-raise instead when the caller must know.
Practice challenge
Use break to exit loop when found.
7
Show a hint
- break stops the loop
- Use break when condition is met
- Only first match will print
Check yourself
1. Why use `with open(...)`?
Show answer
B. It closes the file automatically
2. Which error fires when text can't become a number?
Show answer
B. ValueError
3. Why avoid a bare `except:`?
Show answer
B. It hides bugs like typos
Classes and objects
AdvancedA class bundles data with the operations on that data. Once a set of functions all take the same dictionary as their first argument, that is the moment a class starts paying for itself β you are already passing state around, and a class simply gives that state a name and keeps the functions next to it.
__init__ runs when you create an instance and sets up its attributes. self refers to the instance being worked on; it is passed automatically, which is why you write it in the definition but not at the call site. That asymmetry confuses almost everyone once, and then never again.
The rule that saves real debugging time is where state lives. An attribute defined in the class body rather than inside __init__ is shared by every instance, so one object's change appears on all of them. With a mutable default like a list, this produces bugs that look supernatural β an object you just created already has data in it. Put instance state in __init__ and it cannot happen.
Syntax
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def raise_by(self, pct):
self.salary = round(self.salary * (1 + pct / 100), 2)
return self.salary
def __repr__(self):
return f"Employee({self.name}, {self.salary})"
e = Employee("Ravi", 50000)
e.raise_by(10)
print(e) # Employee(Ravi, 55000.0)
Key points
- self is the instance; you declare it but never pass it when calling e.raise_by(10).
- __init__ is the constructor β it sets initial state, it does not return anything.
- __repr__ gives a readable printout, which makes debugging dramatically easier.
Practice challenge
Parse blame line to get author name.
rajkumar
Show a hint
- Author is before first space
- Split by space and get first
- blame_line.split()[0]
Check yourself
1. What does __init__ do?
Show answer
B. Runs when an instance is created
2. Do you pass self when calling e.raise_by(10)?
Show answer
B. No β Python passes it automatically
3. Why define __repr__?
Show answer
B. A readable printout for debugging
Comprehensions and clean transforms
AdvancedA comprehension builds a list, dict or set in one expression instead of a loop with an append. It is not just shorter β it states the intent. A reader sees 'this produces a list of X where Y' rather than having to trace what a loop accumulates across four lines and decide whether anything else happens in between.
The rule of thumb interviewers use: a comprehension should read as one thought. If you need two conditions and a nested loop, write the loop. Cleverness that takes thirty seconds to read is a cost, not a skill, and the person paying it is usually you in six months. The filter goes at the end β [x for x in items if condition] β and an if before the for means something different entirely.
Generator expressions are the same syntax with round brackets, and they do not build the whole list in memory. For a million-row file that distinction is the difference between a script that runs and one that fills the machine's memory and is killed. Use a comprehension when you need the list, a generator when you only need to iterate once.
Syntax
rows = [
{"name": "Priya", "dept": "data", "salary": 1200000},
{"name": "Sam", "dept": "eng", "salary": 1800000},
{"name": "Ana", "dept": "data", "salary": 1500000},
]
# list comprehension with a filter
data_team = [r["name"] for r in rows if r["dept"] == "data"]
# dict comprehension: name -> salary in lakhs
lakhs = {r["name"]: round(r["salary"] / 100000, 1) for r in rows}
# set comprehension: unique departments
depts = {r["dept"] for r in rows}
print(data_team) # ['Priya', 'Ana']
print(lakhs) # {'Priya': 12.0, 'Sam': 18.0, 'Ana': 15.0}
print(depts) # {'data', 'eng'}
Key points
- The filter goes at the end: [x for x in items if condition]. An if BEFORE the for is a conditional expression and means something different.
- Dict comprehensions need both a key and a value: {k: v for ...}. Forgetting the colon silently gives you a set instead.
- A generator expression uses round brackets and does not build the whole list in memory β use it when you only need to iterate once over something large.
Practice challenge
Define methods and call them on object.
15 5
Show a hint
- Methods take self as first parameter
- self refers to instance
- Call with obj.method(args)
Check yourself
1. What does [n * 2 for n in [1, 2, 3] if n > 1] produce?
Show answer
B. [4, 6]
2. Which builds a dictionary?
Show answer
C. {k: v for k, v in pairs}
3. When should you write a plain loop instead?
Show answer
B. When the comprehension no longer reads as one thought
pandas for real analysis
Job-readypandas is what Python analysts actually spend their day in. A DataFrame is a table held in memory: rows, named columns, and an index. Almost every analyst task is filter, group, aggregate, join β the same four verbs as SQL, in a different notation, which is why analysts who know SQL pick pandas up in days rather than weeks.
The reason it appears in so many analyst job descriptions is not the syntax. It is that a pandas script is repeatable. A spreadsheet answer has to be redone by hand next month and will differ slightly because somebody dragged a formula one row short; a script runs again and produces the same answer. That is what 'automated the weekly report' means on a resume, and it is the single most common thing analysts are actually hired to do.
The trap that catches everyone once is chained assignment. df[df.x > 1]["y"] = 0 operates on a temporary copy, raises SettingWithCopyWarning, and often changes nothing at all β while looking exactly like code that worked. Use .loc[mask, "y"] = 0 instead, which addresses the original frame directly and is unambiguous about what is being modified.
Syntax
import pandas as pd
df = pd.read_csv("applications.csv")
# filter
recent = df[df["applied_on"] >= "2026-01-01"]
# group and aggregate
by_source = (recent
.groupby("source")
.agg(applications=("id", "count"),
interviews=("got_interview", "sum"))
)
by_source["rate"] = (by_source["interviews"] / by_source["applications"] * 100).round(1)
print(by_source.sort_values("rate", ascending=False))
# applications interviews rate
# referral 12 7 58.3
# careers_page 41 9 22.0
# job_board 156 11 7.1
Key points
- df[mask] filters rows; df[["a", "b"]] selects columns. The doubled brackets trip up everyone once.
- groupby().agg() with named arguments produces readable column names instead of a confusing multi-level header.
- Chain operations inside brackets rather than reassigning df at every step β it keeps the transformation readable as a sequence.
Practice challenge
Check if both increase together.
True
Show a hint
- Check if both lists increase together
- all() checks all pairs
- Both increase = positive correlation
Check yourself
1. Which selects two columns?
Show answer
B. df[["a", "b"]]
2. Why is a pandas script preferred to a spreadsheet for a recurring report?
Show answer
B. It repeats identically next month
3. How do you safely set a value on filtered rows?
Show answer
B. df.loc[mask, "y"] = 0
Common questions
Do I need any background to start Python?
No. This track begins at its own beginning and assumes nothing, and the first lesson explains what the thing is before showing you any syntax.
How long does the Python track take?
About 151 minutes of reading across 10 lessons, plus the practice challenges and 30 multiple-choice questions, which is where the time actually goes.
Is it free?
Yes, and there is no account. Everything runs in your browser.
More: all 15 tracks · what employers actually ask for · the full syllabus