open atlas
↑ Back to track
Python for JS/TS developers PY · 01 · 01

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.

PY Middle ◷ 16 min
Level
FoundationsJuniorMiddleSenior
Already know this unit? Take a 1-minute quick check →

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 classify

Variables 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 b

The 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 objects

Rule of thumb: use is only for None, True, False, and sentinel singletons; use == for everything else. Booleans are themselves a subclass of intTrue == 1 and False == 0 are True — another sharp edge when you compare loosely.

Quiz

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 invalid

The 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 / TSPythonNote
let x = 1; const y = 2;x = 1; y = 2No keyword; no built-in const (immutability is the value’s)
a === b (value, no coercion)a == bNo ===; == is the value compare
Object.is(a, b) / ref identitya is bIdentity; use only for None/True/False/sentinels
null and undefinedNoneOne nullish; test with is None
$&#123;name&#125;: $&#123;n&#125;f”{name}: {n}“f-string; holds any expression
number (one IEEE-754 type)int / floatint is arbitrary-precision; / always float, // floors
typeof xtype(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.

Pick the best fit

You must check whether a variable holding an optional value is absent. Pick the correct, idiomatic test.

Recall before you leave
  1. 01
    Explain the difference between == and is in Python, and why `257 is 257` can be False while `256 is 256` is True.
  2. 02
    A 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?
Recap

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.

recallapplystretch0 of 5 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.