Syntax and types: Python through a JS/TS lens
Python blocks are whitespace, names are just references, ints are arbitrary-precision, and type hints never run — coming from JS/TS the gotchas are == vs is and that None is the only nullish.
A JS developer ports a function to Python, runs it, and a == b is True for two lists — but a is b is False, and a colleague’s nearly-identical check returns True for two small integers and False for two large ones. Nothing in the type hints warned them; mypy was never run in CI. They reach for ===, find it doesn’t exist, and assume == is the strict one. It isn’t. Every one of these surprises traces back to four facts about Python’s syntax and type model that are quietly different from JavaScript — and getting them wrong produces code that passes today and breaks on input you didn’t test. By the end of this lesson you’ll know exactly why each of these bites and what to do differently.
Blocks are whitespace; variables are just names
Python has no braces and no semicolons. A block is a run of lines at the same indentation under a header line ending in :. The indentation is the syntax — mixing tabs and spaces, or being off by one space, is a IndentationError, not a style nit. The community standard (PEP 8) is four spaces.
def classify(n: int) -> str:
if n < 0:
return "negative"
elif n == 0:
return "zero"
return "positive" # same level as the if → still in classifyVariables need no keyword. There is no let, const, or var: assignment both declares and binds. And a name is a reference to an object, exactly like a JS object reference — b = a does not copy a list, it makes b point at the same list, so mutating through one is visible through the other. Python has no built-in const; immutability is a property of the value (tuples, strings, frozensets are immutable), not of the binding.
a = [1, 2, 3]
b = a # b references the SAME list, no copy
b.append(4)
print(a) # [1, 2, 3, 4] — mutated through bThe numeric tower: int, float, and two division operators
JavaScript has one number type — an IEEE-754 double — so 9999999999999999 silently loses precision. Python’s int is arbitrary-precision: it grows to whatever size the value needs, no overflow, no silent rounding. float is the IEEE-754 double you know. The trap for JS developers is division: / always produces a float, even on two ints, and // is floor division (rounds toward negative infinity).
type(2 ** 100) # <class 'int'> — 1267650600228229401496703205376, exact
7 / 2 # 3.5 (float, always)
7 // 2 # 3 (floor division, int)
-7 // 2 # -4 (floors toward -infinity, not toward zero)
7 % 3 # 1 (modulo)Strings are immutable sequences. Interpolation is the f-string — Python’s template literal — using {} instead of ${}, and it can hold any expression: f"{name} has {len(items)} items". There is no implicit number-to-string coercion: "x" + 1 is a TypeError, not "x1".
Truthiness, None, and the == vs is gotcha
Like JS, Python has truthiness, but the falsy set differs: False, None, 0, 0.0, "", and every empty collection ([], {}, (), set()) are falsy; everything else is truthy. So if items: is the idiomatic “is this list non-empty?” check — there is no separate length > 0 ceremony.
None is Python’s single nullish value — it replaces both JS null and undefined. There is no undefined; a missing attribute raises, a missing dict key raises, and an absent argument defaults to whatever you declared (commonly None). The idiomatic test is if x is None:, not == None, because None is a singleton and identity is exact and fast.
That brings the real gotcha. == compares value; is compares identity (whether two names point at the literally same object in memory). JS has nothing exactly like is — === is still a value comparison for primitives. Confusing them is the classic bug, and it’s made sneaky by small-int caching: CPython pre-allocates the integers from −5 to 256, so is accidentally returns True for small ints and False for large ones.
a = 256; b = 256
a is b # True — both are the SAME cached object
x = 257; y = 257
x is y # False — two distinct objects (value-equal, not identity-equal)
x == y # True — value comparison, the one you actually wanted
[] == [] # True — equal value
[] is [] # False — two separate list objectsRule of thumb: use is only for None, True, False, and sentinel singletons; use == for everything else. Booleans are themselves a subclass of int — True == 1 and False == 0 are True — another sharp edge when you compare loosely.
x = 257; y = 257. What do x == y and x is y return, and why?
Type hints describe; they do not enforce
If you’re coming from TypeScript with the expectation that annotating a parameter as int actually stops a str from getting through at runtime — this section is the one that will save you a production incident.
This is the deepest break from TypeScript. Python’s type hints (PEP 484) look like TS annotations — def f(x: int) -> str:, count: int = 0, items: list[str] — but the interpreter ignores them at runtime. Passing a str where an int is hinted runs fine until something actually breaks on the value. Hints are checked statically by a separate tool — mypy or pyright — that you run in CI; they are essentially advisory metadata stored in __annotations__.
def repeat(s: str, n: int) -> str:
return s * n
repeat("ab", 3) # 'ababab' — correct
repeat(3, "x") # hints say (str, int) but Python never checks:
# 3 * "x" runs and returns 'xxx' — wrong types, no error, a silent bug
repeat("ab", "3") # "ab" * "3" — TypeError at runtime, since str * str is invalidThe TS contrast is subtle. TS types are also erased at compile time — they don’t exist at runtime either. The difference is ecosystem and enforcement: in TS, code that fails the type-checker normally doesn’t ship (the build fails); in Python, the program runs regardless of whether you ever ran mypy. Treat hints as documentation that a checker can verify, not as a runtime guard. If you need runtime validation, that’s a library (Pydantic, dataclasses with checks), not the type system.
| JS / TS | Python | Note |
|---|---|---|
let x = 1; const y = 2; | x = 1; y = 2 | No keyword; no built-in const (immutability is the value’s) |
a === b (value, no coercion) | a == b | No ===; == is the value compare |
Object.is(a, b) / ref identity | a is b | Identity; use only for None/True/False/sentinels |
null and undefined | None | One nullish; test with is None |
| f”{name}: {n}“ | f-string; holds any expression |
number (one IEEE-754 type) | int / float | int is arbitrary-precision; / always float, // floors |
typeof x | type(x) / isinstance(x, T) | type() returns the class object |
▸Why this works
Why does b = a not copy? Because Python (like JS for objects) has reference semantics: every variable is a name bound to an object, and assignment rebinds the name without touching the object. Mutable objects (list, dict, set) are shared; immutable ones (int, str, tuple, frozenset) can’t be changed in place, so sharing them is invisible. This is also why a mutable default argument — def f(items=[]) — is a famous footgun: the list is created once at definition and shared across every call.
You must check whether a variable holding an optional value is absent. Pick the correct, idiomatic test.
- 01Explain the difference between == and is in Python, and why `257 is 257` can be False while `256 is 256` is True.
- 02A teammate from TypeScript adds type hints everywhere and expects bad calls to fail at runtime like a compiled TS build. What's wrong with that expectation, and what actually enforces the types?
Coming from JS/TS, Python’s syntax and type model rearrange a handful of fundamentals. Blocks are defined by indentation under a header line ending in : — the whitespace is the syntax, and there are no braces or semicolons. Variables need no keyword; assignment declares and binds, and a name is a reference to an object, so b = a shares rather than copies — there is no built-in const, only immutable values. The numeric model splits JS’s single number into an arbitrary-precision int and an IEEE-754 float, with / always yielding a float and // doing floor division. Strings interpolate via f-strings and never coerce numbers implicitly. Truthiness treats 0, "", None, and every empty collection as falsy, and None is the lone nullish value — replacing both JS null and undefined and tested with is None. The sharpest gotcha is == (value) versus is (identity): they disagree on equal-but-distinct objects, and small-int caching makes is deceptively return True for ints in −5..256. Finally, type hints look like TS annotations but are never enforced at runtime; mypy or pyright checks them statically, and unlike a TS build that blocks on type errors, a Python program runs regardless — so treat hints as verifiable documentation, not a guard. Now when you see a reviewer write if x is None where you’d write == None, or strip your [] default to None, you’ll know exactly why — and you’ll do the same.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.