Skip to main content

Basics

1. Python Fundamentals

Python Type Hierarchy​

Numbers​

  • Integral
    • Integers
    • Booleans
  • Non-Integral
    • Floats (C doubles)
    • Complex
    • Decimals
    • Fractions

Collections​

  • Sequences
    • Mutable: Lists
    • Immutable: Tuples, Strings
  • Sets
    • Mutable: Sets
    • Immutable: Frozen Sets
  • Mappings
    • Dictionaries

Callables​

  • User-Defined Functions
  • Generators
  • Classes
  • Instance Methods
  • Class Instances (__call__())
  • Built-in Functions (e.g. len(), open())
  • Built-in Methods (e.g. my_list.append(x))

Singletons​

  • None
  • NotImplemented
  • Ellipsis (...)

Naming Conventions​

Identifier Names​

  • Case-sensitive: my_var, my_Var, ham, Ham are all different identifiers
  • Must follow certain rules
  • Should follow certain conventions

Rules (Must)​

  • Must start with an underscore (_) or a letter (a-z, A-Z)
  • Followed by any number of underscores (_), letters (a-z, A-Z), or digits (0-9)
  • Legal examples: var, my_var, index1, index_1, _var, __var, __lt__
  • Cannot be a reserved word:
    • None, True, False
    • and, or, not
    • if, else, elif
    • for, while, break, continue, pass
    • def, lambda, global, nonlocal, return, yield
    • del, in, is, assert, class
    • try, except, finally, raise
    • import, from, with, as

Conventions​

  • _my_var (single underscore) — convention indicating "internal use" or "private" objects. Objects named this way are not imported by from module import *
  • __my_var (double underscore, leading) — used to "mangle" class attributes; useful in inheritance chains
  • __my_var__ (double underscore, leading and trailing) — used for system-defined names with special meaning to the interpreter (e.g. __init__). Don't invent your own — stick to Python's predefined ones.
    • Example: x < y calls x.__lt__(y)

Other Naming Conventions (from PEP 8 Style Guide)​

TypeConventionExample
Packagesshort, all-lowercase names; preferably no underscoresutilities
Modulesshort, all-lowercase names; can have underscoresdb_utils, dbutils
ClassesCapWords (upper camel case)BankAccount
Functionslowercase, snake_caseopen_account
Variableslowercase, snake_caseaccount_id
Constantsall-uppercase, underscore-separatedMIN_APR

Multi-Line Statements​

Physical vs. Logical Lines​

  • Physical line of code — ends with a physical newline character
  • Logical line of code — ends with a logical NEWLINE token (after tokenizing)
  • Sometimes physical newlines are ignored in order to combine multiple physical lines into a single logical line of code, terminated by a logical NEWLINE token
  • Conversion between physical and logical lines can be implicit or explicit

Implicit Line Joining​

Happens automatically inside expressions in:

  • list literals: [ ]
  • tuple literals: ( )
  • dictionary literals: { }
  • set literals: { }
  • function arguments / parameters

Supports inline comments.

Example:

[1, [1, #item 1
2, 2, #item 2
3] 3 #item 3
]

def my_func(a,
b, #comment
c):
print(a, b, c)

my_func(10, #comment
20, 30)

Explicit Line Joining​

  • Break up statements over multiple lines explicitly using the \ (backslash) character
  • Multi-line statements are not implicitly converted to a single logical line — the backslash is required
  • Comments cannot be part of a statement, not even a multi-line statement

Example (valid):

if a \
and b \
and c:

Example (invalid — comment breaks it):

if a \
and b \ #comment
and c:

Multi-Line String Literals​

  • Created using triple delimiters: '''...''' (single) or """...""" (double)
  • Non-visible characters (newlines, tabs, etc.) are actually part of the string — basically anything you type
  • Can use escaped characters (e.g. \n, \t), string formatting, etc.
  • A multi-line string is just a regular string
  • Multi-line strings are not comments, although they can be used as such — especially as docstrings

Imports​

from module import name lets you import specific items from a module directly into your namespace, so you use them without the module prefix.

Basic forms​

from math import sqrt
print(sqrt(16)) # 4.0, no need for math.sqrt

from math import sqrt, pi, ceil # multiple names

Import everything (generally discouraged)​

from math import *

This pollutes your namespace and can cause name clashes — avoid it in real code.

Renaming with as​

from math import sqrt as square_root
print(square_root(25)) # 5.0

From a package/submodule​

from os import path
from os.path import join, exists
from collections import OrderedDict

Relative imports (within a package)​

from . import sibling_module # same package
from .subpackage import something # subpackage
from ..parent_module import thing # parent package

vs. plain import​

import math
math.sqrt(16) # need the prefix

from math import sqrt
sqrt(16) # no prefix needed
  • Use import module when you want clarity about where functions come from.
  • Use from module import name when you're using something frequently and want shorter code.

Modules and Packages​

Module​

A single Python file containing code (functions, classes, variables).

# mymodule.py
def greet():
print("hello")
import mymodule
mymodule.greet()

Package​

A directory containing multiple modules, plus an __init__.py file (marks it as a package). A way to organize related modules into a hierarchy.

mypackage/
__init__.py
module1.py
module2.py
subpackage/
__init__.py
module3.py
import mypackage.module1
from mypackage import module2
from mypackage.subpackage import module3

Key differences​

ModulePackage
What it isA single .py fileA directory of modules
RequiresNothing special__init__.py (optional marker in Python 3.3+, but still common practice)
PurposeGroups related codeGroups related modules
Examplemath, os.path's pathos, numpy, collections

Analogy: a module is like a single chapter (file), a package is like a book made of multiple chapters organized in a folder.

Real-world example​

import numpy # numpy is a package
numpy.array([1, 2]) # array is defined in one of numpy's modules, but numpy re-exports it

import numpy.random # random is a submodule inside the numpy package

Every package is (usually) made of modules, but a module isn't necessarily part of a package — it can just be a standalone .py file.


2. Operators & Expressions

Ternary Operator​

Syntax (conditional expression):

value = true_result if condition else false_result

Example:

x = 5
label = "positive" if x > 0 else "non-positive"
print(label) # "positive"

Extras​

  • Chained: a if cond1 else b if cond2 else c
  • In a function call: print("yes" if flag else "no")
  • As a default value: name = user_input if user_input else "Guest" (or user_input or "Guest" for falsy-checks)
  • If multiple statements are needed per branch, use a regular if/else block instead — the ternary is expression-only.

The not Operator​

not can look like a function call, but isn't one:

not(True) # False
not (x > 5) # works fine
  • This works, but not because not is a function being called with parentheses.
  • The parentheses here are just a normal grouping expression — same as writing (True) or (x > 5) — and not is applied to whatever that parenthesized expression evaluates to.
  • So not(x > 5) is really parsed as not (x > 5), functionally identical to just not x > 5 (since not has lower precedence than comparison operators anyway).

Where this can trip people up​

not (1, 2, 3) # False — this is NOT "not applied to each element"
  • (1, 2, 3) is a tuple, and a non-empty tuple is truthy, so not on it gives False.
  • If coming from a language where not(...) looks like a function call, it's easy to misread this.

Bottom line​

  • not(expr) is valid and common, but it's not applied to a parenthesized expression — not a function call.
  • Style-wise, not (expr) (with a space) or just not expr is usually preferred for clarity, since the parens can visually suggest a function call that isn't actually happening.

is vs ==​

The is operator checks identity — whether two variables point to the exact same object in memory — as opposed to ==, which checks equality of values.

Basic behavior​

a = [1, 2, 3]
b = [1, 2, 3]
c = a

a == b # True — same values
a is b # False — different objects in memory
a is c # True — c refers to the same object as a

Under the hood, a is b is equivalent to id(a) == id(b) — ties back to id().

Common correct use: comparing to None​

if x is None:
print("x is None")

This is the idiomatic way to check for None — not x == None. Why? Because is guarantees checking against the singleton None object itself, and it can't be fooled by a custom __eq__ override. PEP 8 explicitly recommends is None / is not None.

if x is not None:
...

The classic gotcha: small integers and strings​

a = 5
b = 5
a is b # True — small ints are cached/interned by CPython

x = 1000
y = 1000
x is y # False (usually) — large ints aren't guaranteed to be cached

s1 = "hello"
s2 = "hello"
s1 is s2 # True — string literals are often interned

s3 = "hello world!"
s4 = "hello world!"
s3 is s4 # may be True or False — not guaranteed

This caching behavior is a CPython implementation detail, not a language guarantee — relying on it for anything other than None/True/False checks is a bug waiting to happen.

is vs == — when to use which​

is==
Checksidentity (same object)equality (same value)
Use forNone, True, False, sentinel objectsalmost everything else
Can be customizedNoYes, via __eq__
class AlwaysEqual:
def __eq__(self, other):
return True

x = AlwaysEqual()
x == "anything" # True — __eq__ overridden
x is "anything" # False — is can't be fooled

is not​

if x is not None:
...

Reads more naturally than not x is None (also legal but less common).

Rule of thumb: use is only for None, True, False, and singleton/sentinel checks. Use == for comparing actual values.


3. Control Flow & Iteration

No Switch, No Do-While​

  • Python has no switch statement
  • Python has no do-while loop
    • Can emulate a do-while easily using while True:

Loop else Clauses​

while...else​

  • The else block runs only if the loop finishes because its condition became False
  • It does not run if the loop exits via break

else vs finally​

  • finally (a try/except construct) runs unconditionally, no matter what happens
  • else on a loop is conditional — it only runs if break was never hit

for...else​

  • Same rule as while...else, just applied to exhausting an iterable instead of a condition going false
  • Both while...else and for...else are handy for "search" patterns — lets you skip a manual flag variable to track whether something was found

For Loops and Iterables​

  • No C-style for loop in Python
  • Only the for...in loop
  • Works with iterables
  • range() creates an iterable
  • Collections and iterables are not the same thing
  • Strings and tuples are iterables

Unpacking​

The correct term is unpacking (also iterable unpacking or tuple unpacking, though it works on any iterable, not just tuples). Python's official docs and PEP 3132 refer to it as "unpacking."

Basic unpacking​

a, b, c = (1, 2, 3)
a, b, c = [1, 2, 3] # works with lists too
a, b, c = "xyz" # even strings! a='x', b='y', c='z'

The right-hand side just needs to be iterable with the right number of items.

Starred unpacking (PEP 3132)​

first, *middle, last = [1, 2, 3, 4, 5]
# first = 1, middle = [2, 3, 4], last = 5

*head, tail = [1, 2, 3]
# head = [1, 2], tail = 3

The starred name collects "everything else" into a list. Only one * is allowed per unpacking.

Swapping without a temp variable​

a, b = b, a

This is unpacking in disguise — the right side builds a tuple (b, a), then it's unpacked into a, b.

Function calls: unpacking arguments​

def add(x, y, z):
return x + y + z

nums = [1, 2, 3]
add(*nums) # unpacks list into positional args

d = {'x': 1, 'y': 2, 'z': 3}
add(**d) # unpacks dict into keyword args

* unpacks iterables into positional arguments; ** unpacks mappings into keyword arguments.

Unpacking into function definitions​

def f(a, b, *args, **kwargs):
print(a, b, args, kwargs)

f(1, 2, 3, 4, x=5)
# a=1, b=2, args=(3, 4), kwargs={'x': 5}

Here *args/**kwargs collect rather than unpack — same syntax, opposite direction, which trips people up.

Merging with unpacking (PEP 448)​

combined = [*[1, 2], *[3, 4]] # [1, 2, 3, 4]
merged = {**{'a': 1}, **{'b': 2}} # {'a': 1, 'b': 2}

Nested unpacking​

((a, b), c) = ((1, 2), 3)
# a=1, b=2, c=3

Summary of terms​

  • Unpacking — the general term, assigning iterable contents to multiple targets
  • Starred expression / extended unpacking — the *name form (PEP 3132)
  • Argument unpacking — using */** in function calls
  • Argument packing (less formal term) — using *args/**kwargs in function definitions

enumerate()​

enumerate() is a built-in Python function that adds a counter to an iterable, giving you both the index and the value as you loop.

Basic usage​

fruits = ['apple', 'banana', 'cherry']
for i, fruit in enumerate(fruits):
print(i, fruit)
# 0 apple
# 1 banana
# 2 cherry

Without enumerate(), you'd have to do this manually:

i = 0
for fruit in fruits:
print(i, fruit)
i += 1

or the clunkier range(len(...)) pattern:

for i in range(len(fruits)):
print(i, fruits[i])

enumerate() is the idiomatic, Pythonic way to avoid both.

Custom start value​

for i, fruit in enumerate(fruits, start=1):
print(i, fruit)
# 1 apple
# 2 banana
# 3 cherry

What it actually returns​

enumerate() returns an enumerate object — an iterator that yields (index, value) tuples. That's why it pairs naturally with tuple unpacking in a for loop:

list(enumerate(['a', 'b']))
# [(0, 'a'), (1, 'b')]

Each pair is a tuple, and for i, val in enumerate(...) is unpacking each tuple as it's produced — ties back to Unpacking.

Works with any iterable, not just lists​

for i, char in enumerate("abc"):
print(i, char)
# 0 a
# 1 b
# 2 c

Common use case: building a dict from a list​

fruits = ['apple', 'banana', 'cherry']
index_map = {fruit: i for i, fruit in enumerate(fruits)}
# {'apple': 0, 'banana': 1, 'cherry': 2}

Signature: enumerate(iterable, start=0) — takes any iterable and an optional starting index, defaults to 0.


4. Built-in Functions

id(), hex(), str()​

Three built-in global functions, each doing something quite different.

id()​

Returns the identity of an object — a unique integer for that object's lifetime (in CPython, this is typically the memory address).

x = [1, 2, 3]
id(x) # e.g. 140234567891200
  • Used to check if two variables refer to the same object (not just equal values):
a = [1, 2]
b = [1, 2]
a == b # True (same value)
a is b # False (different objects)
id(a) == id(b) # False
  • is internally compares id() values.

hex()​

Converts an integer to its hexadecimal string representation (prefixed with 0x).

hex(255) # '0xff'
hex(16) # '0x10'
hex(-10) # '-0xa'
  • Only works on integers (or objects with __index__), not floats or strings.
  • Related functions: bin() (binary), oct() (octal).
  • To go the other way (string → int), use int(s, 16):
int('0xff', 16) # 255
int('ff', 16) # 255

str()​

Converts an object into its string representation. One of the most commonly used built-ins.

str(42) # '42'
str(3.14) # '3.14'
str([1, 2, 3]) # '[1, 2, 3]'
str(True) # 'True'
str(None) # 'None'
  • Also the type/class for strings — str is a type, and calling str(x) invokes x.__str__() under the hood.
  • Different from repr(): str() aims for readability, repr() aims for an unambiguous, code-like representation.
class Point:
def __str__(self):
return "a point"
def __repr__(self):
return "Point()"

p = Point()
str(p) # 'a point'
repr(p) # 'Point()'

Quick connection between the three​

x = 255
id(x) # some big int, e.g. 4327462800
hex(id(x)) # hex string of that memory address, e.g. '0x101a2b6d0'
str(hex(id(x))) # same thing, since hex() already returns a str

type() vs isinstance()​

Both check an object's type, but they differ in how strictly and how they handle inheritance.

Basic usage​

x = 5
type(x) # <class 'int'>
type(x) == int # True
isinstance(x, int) # True

Key difference: inheritance​

isinstance() respects subclassing — returns True if the object is an instance of the class or any subclass of it. type() == does an exact match only.

class Animal:
pass

class Dog(Animal):
pass

d = Dog()

isinstance(d, Animal) # True — Dog is a subclass of Animal
type(d) == Animal # False — type(d) is exactly Dog, not Animal
type(d) == Dog # True

This is the most important practical difference, and it's why isinstance() is almost always preferred for type checks — it works correctly with polymorphism.

bool and int — a classic gotcha​

type(True) == int # False — type(True) is exactly bool
isinstance(True, int) # True — bool is a subclass of int!

Since bool subclasses int in Python, isinstance sees True/False as also being ints. This trips people up constantly.

Checking multiple types at once​

isinstance() accepts a tuple of types — type() has no equivalent shortcut:

isinstance(x, (int, float)) # True if x is int OR float

Duck typing / __instancecheck__​

isinstance() can be customized via metaclasses (__instancecheck__), and works with Abstract Base Classes:

from collections.abc import Iterable

isinstance([1, 2, 3], Iterable) # True
isinstance("abc", Iterable) # True

type() comparisons can't hook into this — they only ever check the literal, exact class.

When you'd actually want type() == instead​

Rare, but useful when deliberately wanting to exclude subclasses:

def strict_check(x):
if type(x) == list: # only exact lists, not subclasses of list
...

Shows up in edge cases like preventing subclass instances from being treated identically to the base class — but uncommon, and often a sign to reconsider the design.

Summary​

type() ==isinstance()
Exact match onlyYesNo — includes subclasses
Handles bool/int quirkNoYes (bool counts as int)
Multiple types at onceNoYes, via tuple
Works with ABCsNoYes
Pythonic / recommendedRarelyAlmost always

Rule of thumb: use isinstance() unless there's a specific reason to reject subclasses.


5. Classes & OOP

Access Modifiers​

  • No access modifiers in Python classes (no private/protected/public keywords)
  • Use _var as convention instead

Getters and Setters​

The idiomatic approach uses the @property decorator.

class Person:
def __init__(self, name):
self._name = name # convention: leading underscore for "private"

@property
def name(self): # getter
return self._name

@name.setter
def name(self, value): # setter
if not value:
raise ValueError("name cannot be empty")
self._name = value

Usage looks like plain attribute access — no parentheses:

p = Person("Alice")
print(p.name) # calls the getter -> "Alice"
p.name = "Bob" # calls the setter
p.name = "" # raises ValueError

Key points​

  • _name is the actual storage attribute; name is the public-facing property.
  • The getter method is decorated with @property.
  • The setter uses @<propertyname>.setter and must have the same method name.
  • Can also add a @name.deleter for del p.name if needed.
  • If only the getter is defined (no setter), the property becomes read-only.
  • Preferred over Java-style get_name()/set_name() methods because it lets you start with plain attributes and add validation/logic later without changing the public interface.
  • The getter and setter share the same name (name), but this is not method overloading — Python has no overloaded methods.
  • Python uses raise to raise an exception (not throw, as in Java/C#/JS).

Classes​

Defining a class​

class Rectangle:
def __init__(self, height, width):
self.height = height
self.width = width
  • class Rectangle: — creates an empty object (the class), passed to __init__
  • __init__ runs right after object creation
  • self refers to the new object — can technically be named anything, but "self" is convention
  • Inside __init__, lines like self.height = height set ("get") props on the new object

Instance methods​

def area(self): # instance method
return self.height * self.width
  • The self parameter name is optional/conventional, not enforced by the language

Calling​

r1 = Rectangle(10, 20)
r1.area()
  • r1.area() automatically passes r1 as the self argument — kind of similar to this binding in JS

Special methods in a class​

  • __str__(self) — like toString() in JS; works with str(), e.g. str(r1)
  • __repr__(self) — representation as a string; conventionally should let you recreate the object again
  • __eq__(self, other) — like Java's .equals(); works with the == operator
  • __lt__, __le__, __gt__, __ge__ — similar to __eq__, for <, <=, >, >=
  • __ne__ — not equal (!=)