Modern Python Basics #2: Variables, basic types, and type hints
We continue from the project we made in #1 Getting started and uv setup. The topic of this post is variables, basic types, and type hints.
Python is a dynamically typed language. You don’t have to write types on variables. Yet modern Python writes types from the start — that’s now the standard. This post explains why and how.
Variables — no separate declaration #
The biggest difference from other languages. There’s no let, var, or int x keyword. Just assign a value to a name and that’s a variable.
name = "커티스"
age = 30
height = 175.5
is_admin = TrueThe naming convention is snake_case. It’s different from JavaScript’s camelCase, which feels strange at first. Constants use uppercase + underscore (MAX_RETRY = 3).
Basic types — 4 + 1 #
Pin down the five most-used:
i: int = 42
f: float = 3.14
s: str = "hello"
b: bool = True
n: None = NoneThe : int part is a type hint. After the variable name, write a colon and a type. It doesn’t affect behavior, but tools (IDEs/type checkers) read it for validation and autocomplete.
Why annotate types in a dynamically typed language? #
Type hints are simply ignored at runtime. The following code runs fine.
x: int = "this is a string"
print(x) # this is a stringReasons to write them anyway:
- Editor autocomplete and go-to-definition become accurate — VS Code and PyCharm both read types
- Type checkers (mypy / pyright / Pyrefly) catch problems at “compile” time
- Acts as documentation when others read your code — function signatures alone tell what’s taken and returned
- Refactor safety net — change a function signature and you see what breaks
Old Python code lacks types. The modern Python convention is to add them wherever possible in new code.
Type hints — variables, functions, collections #
Variables #
count: int = 0
name: str
name = "curtis" # declare type first, assign value latername: str is also valid — type only, no value. Common in class field declarations.
Functions #
def add(a: int, b: int) -> int:
return a + b
def greet(name: str) -> None:
print(f"hi, {name}")After parameters: : type; for return: -> type. If nothing returns, -> None. Functions without these leave the IDE guessing at the return type, which dims autocomplete for callers as well.
Collections — built-in generics #
For collections like lists and dicts, annotate element types too.
nums: list[int] = [1, 2, 3]
names: list[str] = ["a", "b"]
ages: dict[str, int] = {"curtis": 30, "smith": 25}
unique: set[str] = {"a", "b"}
point: tuple[float, float] = (1.0, 2.0)The syntax of putting [] directly on built-in types like list[int] is supported from Python 3.9. Before that, it was from typing import List then List[int]. You’ll still see it in old code, but new code unifies on the built-in form.
from typing import List, Dict
nums: List[int] = [1, 2, 3]
ages: Dict[str, int] = {"curtis": 30}None and option types — int | None
#
For fields where a value may or may not exist.
def find_user(id: int) -> str | None:
if id == 1:
return "curtis"
return Nonestr | None is the union shorthand supported from Python 3.10. Before that you had to import Optional[str] or Union[str, None] from typing.
from typing import Optional, Union
def find_user(id: int) -> Optional[str]: ...
def parse(value: str) -> Union[int, float, None]: ...New code always uses |. Short, no extra import.
def parse(value: str) -> int | float | None:
try:
return int(value)
except ValueError:
try:
return float(value)
except ValueError:
return NoneNumbers — int and float #
Python’s int is arbitrary precision. No 64-bit limit.
big = 10 ** 100
print(big)
# 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000float is IEEE 754 double. The same floating-point gotchas as other languages apply.
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # FalseFor finance or exact decimals, use decimal.Decimal.
from decimal import Decimal
print(Decimal("0.1") + Decimal("0.2")) # 0.3Small tip — numeric literals #
You can insert underscores for readability (3.6+).
ONE_MILLION = 1_000_000
HEX = 0xFF_FF
BIN = 0b_1010_1010Strings — str and f-strings
#
There’s no difference between single and double quotes. Use one consistently per project (usually ").
s1 = "hello"
s2 = 'world'
s3 = """여러 줄
문자열"""For interpolation, use f-strings.
name = "curtis"
age = 30
print(f"hi, {name}! you are {age} years old.")
print(f"next year you'll be {age + 1}.")
print(f"{name = }") # name = 'curtis' (handy for debugging)The f"{var = }" form prints the variable name and the value together. Used a lot for debug prints.
t-strings — Python 3.14’s new syntax #
PEP 750 added t-strings in Python 3.14. They look like f-strings, but interpolation does not happen immediately; the result is a Template object instead.
from string.templatelib import Template
name = "curtis"
tpl: Template = t"hi, {name}!"
# Template object. Not yet joined into a string.
# Pass to a function that handles it safely
def render_html(template: Template) -> str: ...
html = render_html(t"<a href='{user_url}'>{user_name}</a>")When to use? Places where user input must be handled safely — SQL, HTML, shell. With f-strings, the interpolated result is a string immediately, so it’s easy to forget to escape; with t-strings, the library carries the escape responsibility. At the entry-level, “knowing f-strings is enough”; just recognize t-strings when you encounter them in library code.
bool and truthiness
#
There are two values, True and False, and bool is a subtype of int.
print(True + True) # 2
print(isinstance(True, int)) # TrueInteresting, but mixing int where bool is expected isn’t recommended. Use True/False explicitly.
Truthy / falsy #
Empty containers and 0 are false; everything else is true.
if not []: print("empty list is falsy")
if not "": print("empty str is falsy")
if not 0: print("zero is falsy")
if not None: print("None is falsy")Expressions like if not items: are common. Shorter than JavaScript’s if (!arr.length).
Type conversion — explicit #
Python performs almost no implicit type coercion. Adding an int and a string raises an error.
n = 42
s = "answer: " + str(n) # must convert with str(n)
# "answer: " + n → TypeError
age = int(input("나이: ")) # input returns str → convert to intCommon conversions:
str(42) # '42'
int("42") # 42
int("42", 16) # 66 (interpret as hex)
float("3.14") # 3.14
bool(0) # False
bool("any") # True (only the empty string is False)
list("abc") # ['a', 'b', 'c']Type aliases — the type keyword
#
When the same type shape repeats, name it.
type UserId = int
type UserName = str
type UserMap = dict[UserId, UserName]
def get_user(id: UserId) -> UserName | None: ...The type syntax was added in Python 3.12. Before that, it was written as:
UserId = int # just a variable — works as an alias
# or
from typing import TypeAlias
UserId: TypeAlias = intNew code unifies on type.
Type checkers — mypy / pyright #
Since type hints aren’t validated at runtime, a separate tool does static checking. Two camps:
- mypy — the oldest standard. Python-camp tool.
uv add --dev mypy - pyright / Pylance — Microsoft’s. Fast, with strong VS Code integration.
uv add --dev pyright - Pyrefly — Meta’s newest (still beta as of 2026). Faster next-gen contender
For a new project, starting with pyright is reasonable. With the Python extension in VS Code, Pylance kicks in automatically and you barely need any configuration.
uv add --dev pyright
uv run pyright .A small example to see it in action:
def add(a: int, b: int) -> int:
return a + b
result = add("hello", 1) # ✗ str is not int$ uv run pyright check.py
check.py:4:18 - error: Argument of type "Literal['hello']" cannot be assigned to parameter "a" of type "int"Caught before runtime. It can feel like nagging at first, but after a month you’ll wonder how you lived without it.
Wrap-up #
What this post covered:
- Python has no separate variable declaration — assignment = declaration
- Five basic types:
int,float,str,bool,None - Type hints are ignored at runtime, yet always writing them is the modern Python convention
- Use built-in generics for collections:
list[int],dict[str, int],tuple[int, str] - Options are
T | None(3.10+); oldOptionalisn’t used in new code - f-string
f"{var}"and debug-friendlyf"{var = }"; t-strings for safe interpolation intis arbitrary-precision,floatis IEEE 754; useDecimalwhen accuracy matters- Type aliases are
type Name = ...(3.12+) - Static type validation with mypy or pyright
In the next post (#3 Control flow) we cover flow control — if, while, for, and the match-case added in 3.10. The key is how it differs from switch in other languages.