This is the crash course I wish someone had handed me: what you'll actually use, the errors you'll actually hit, and permission to skip the rest for now. Read it, or hit ▶ Listen on any section and learn while you're making coffee.
Name on the left, value on the right, done. No type declarations, no semicolons. Skip type hints for now too — you'll meet them later and they'll make sense then.
name = "Ada" # string
age = 36 # integer
height = 1.7 # float
is_ready = True # capital T — true will crash
nothing = None # Python's "null", capital N
print(name, age) # Ada 36
NameError: name 'true' is not definedTrue/False, not true/false. If you come from JavaScript, you'll type the lowercase version at least three times this week.
Python has three ways to format strings. Ignore two of them. % formatting and .format() exist in old tutorials and old codebases — you only ever need to write f-strings: an f before the quote, variables in braces.
name = "Ada"
msgs = 5
print(f"Hi {name}, you have {msgs} messages")
# the four methods that cover 90% of real string work:
s = " Swift Grasp "
s.strip() # "Swift Grasp" — trim whitespace
s.lower() # for case-insensitive compares
s.split() # ['Swift', 'Grasp']
", ".join(["a", "b"]) # "a, b" — yes, join lives on the separator
TypeError: can only concatenate str (not "int") to str"age: " + 36. Python refuses to glue text to numbers. Don't sprinkle str() everywhere — just use an f-string: f"age: {36}" and the problem never comes back.
There are no curly braces. A colon opens a block, and the indentation is the syntax. This is the number one source of day-one crashes, so let's deal with it head-on.
score = 87
if score >= 90:
print("A")
elif score >= 80:
print("B") # this one runs
else:
print("keep going")
# and / or / not are words, not && || !
if 80 <= score < 90: # chained compare — very Python
print("solid B")
label = "pass" if score >= 60 else "fail"
IndentationError: unexpected indent or its evil twin TabError: inconsistent use of tabs and spacesThe container you'll use daily: ordered, mixable, grows as needed. Two ideas carry most of the weight — negative indexes count from the end, and slices cut ranges out.
langs = ["python", "js", "rust"]
langs.append("go") # add to end
langs[0] # "python"
langs[-1] # "go" — last item, no len() dance
langs[1:3] # ['js', 'rust'] — end index not included
len(langs) # 4
"rust" in langs # True
nums = [3, 1, 2]
sorted(nums) # new sorted copy: [1, 2, 3]
nums.sort() # sorts in place, returns None (!)
IndexError: list index out of rangelangs[4] is off the end. When it's the last item you want, reach for langs[-1] instead of langs[len(langs)-1]; the second one is where these bugs breed.
Key–value pairs, like a JSON object. I'd estimate half of working Python is passing dicts around, so learn one habit early: square brackets when the key must exist, .get() when it might not.
user = {"name": "Ada", "age": 36}
user["name"] # "Ada"
user["role"] = "admin" # add or overwrite
user.get("email") # None — no crash
user.get("email", "n/a") # "n/a" — with a default
for key, value in user.items():
print(f"{key} = {value}")
KeyError: 'email'.get() with a default is the calm version.
Python's for walks the items directly — if you're writing for i in range(len(things)), stop, there's always a cleaner way. Need the index too? That's what enumerate is for.
for lang in ["python", "js", "rust"]:
print(lang)
for i, lang in enumerate(["a", "b"]):
print(i, lang) # 0 a, then 1 b
for i in range(3): # just numbers: 0, 1, 2
print(i)
count = 3
while count > 0:
count -= 1 # there is no count-- in Python
for x in items[:], or build a new list with a comprehension (next section).
Define with def, hand back results with return. Defaults make arguments optional, and calling with names — greet(excited=True) — keeps call sites readable six months later.
def greet(name, excited=False):
msg = f"Hello, {name}"
return msg + "!" if excited else msg
greet("Ada") # Hello, Ada
greet("Ada", excited=True) # Hello, Ada!
# returning two things (it's a tuple, unpacked on arrival)
def min_max(nums):
return min(nums), max(nums)
lo, hi = min_max([3, 1, 9]) # lo=1, hi=9
def add(item, items=[]). That default list is created once and shared across every call — items from call one show up in call two. Every Python developer gets bitten by this exactly once. Use items=None, then if items is None: items = [] inside.
Build a new list in one line: transform on the left, loop in the middle, filter on the right. One honest rule — if it needs more than one if or a nested loop, write a normal loop instead. Clever one-liners are how you lose an afternoon.
nums = [1, 2, 3, 4, 5]
squares = [n * n for n in nums] # [1, 4, 9, 16, 25]
evens = [n for n in nums if n % 2 == 0] # [2, 4]
# dict version
lengths = {w: len(w) for w in ["hi", "hello"]}
# {'hi': 2, 'hello': 5}
Always open files with with — it closes them for you even if the code crashes halfway. And always pass encoding="utf-8". Tutorials skip it because it works on their Mac; on Windows the default encoding is different and real files will bite you.
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("line one\n")
with open("notes.txt", encoding="utf-8") as f:
for line in f: # line by line, any file size
print(line.strip())
# JSON in and out — you'll do this weekly
import json
with open("data.json", "w", encoding="utf-8") as f:
json.dump({"name": "Ada"}, f)
UnicodeDecodeError: 'charmap' codec can't decode byte 0x9dencoding="utf-8" on Windows. The fix is the habit above; the six characters of typing are cheaper than the hour of confusion.
Two skills here. First: when a traceback appears, read the last line first — that's the actual error; the lines above it are just the trail. Second: catch specific error types. A bare except: swallows every bug in the block and you will spend hours finding what it ate.
try:
age = int("not a number")
except ValueError:
print("that wasn't a number")
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"oops: {e}")
# raise your own when input is nonsense
def set_age(age):
if age < 0:
raise ValueError("age can't be negative")
Traceback (most recent call last): … means read from the bottom up. Last line = what went wrong. Second-to-last = the exact file and line. Everything above = how the program got there. Most beginners read top-down and get lost in framework internals.
Honest take: you can write useful Python for weeks without defining a class, so don't stall here. Learn to read them — __init__ runs at creation, self is the object talking about itself — and move on. When your dicts start feeling unruly, come back.
class Dog:
def __init__(self, name):
self.name = name
self.tricks = []
def learn(self, trick):
self.tricks.append(trick)
return f"{self.name} learned {trick}"
rex = Dog("Rex")
rex.learn("sit")
rex.tricks # ['sit']
TypeError: learn() takes 1 positional argument but 2 were givenself in the method definition. Every method's first parameter is self — Python passes the object in automatically, so the counts look off-by-one until this clicks.
The lines everyone googles a hundred times. Bookmark this section; that's what it's for.
# swap two variables
a, b = b, a
# reverse a list or string
nums[::-1]
# dedupe, keeping order (list(set(x)) scrambles order — avoid)
list(dict.fromkeys(items))
# most common element
from collections import Counter
Counter(words).most_common(1)
# merge two dicts (right side wins on conflicts)
merged = {**defaults, **overrides}
# read input from the user
name = input("Name: ")
# install a package / run a script (in the terminal)
# pip install requests
# python script.py
That's the working core of the language. The way it sticks is not more reading — pick something tiny and annoying in your life (renaming a folder of files, a to-do list in the terminal) and build it. You now know enough.