Colon Operators in Programming: `::`, `?:`, and `:` Across Languages
Originally published on tamiz.pro. The colon is one of the most overloaded characters in programming. It separates keys from values, annotates types, scopes names, and drives ternary expressions — often several roles at once in a single file. This reference maps the colon's responsibilities across mainstream languages, so you can read unfamiliar syntax without guessing. Quick Lookup Table Syntax Languages Role Precedence / Notes key: value YAML, JSON, TypeScript, CSS Map/record entry Lowest — structural, not an operator label: C, Assembly Jump target Resolved at link/assembly time x: T TypeScript, Swift, Rust (let) Type annotation Right-associative, binds tightly case v: C, Java, Go, Rust Match arm Ends with break/fallthrough rules T::method() C++ Static scope resolver Member access precedence mod::item Rust Path separator Left-to-right, like . a ? b : c C, Java, JS, Swift Ternary conditional Low, right-associative x if cond else y Python Conditional expression Lower than most operators dict['k'] Python Key access Subscript precedence slice[a:b] Python Slicing Exclusive upper bound {'k': v} Python, JS, Ruby Dict/hash literal Braces delimit scope time 12:30:45 HCL (Terraform) Literal delimiter Inside string/time context :: C++20, Ada, OCaml Namespace/global qualifier Left-associative label@loop: ASM Symbol + attribute Assembler-specific Python: Annotations, Dicts, Slices, Labels Python uses : in four distinct roles, all visible in ordinary code. from typing import TypedDict class User(TypedDict): name: str # type annotation in a class body age: int def greet(user: User, /, *, lang: str = "en") -> str: # ^ parameter annotation ^ return annotation return f"Hello, {user['name']}" scores = {"alice": 95, "bob": 87} # dict literal: key: value subset = scores["alice":"bob"] # invalid on dict; valid on list slices data = [0, 1, 2, 3, 4] print(data[1:4]) # [1, 2, 3] — end-exclusive slice print(data[::2]) # [0, 2, 4] — step Type annotations (name: T) appear in function signatures, variable declarations (3.6+ with x: int = 5), and class bodies. They are evaluated lazily or stored as strings under from __future__ import annotations. Dict literals and comprehensions use key: value. The colon never appears as an operator inside a dict — it is a separator token produced by the tokenizer. Slicing a[start:stop:step] compiles to __getitem__ with a slice object. The upper bound is exclusive, matching C conventions. Conditional expressions use the word if/else, not ?: — Python deliberately avoids the ternary colon to reduce ambiguity with slices and dict literals. Edge case: a dict key can be any hashable expression, so {f"{a}": b for a, b in pairs} parses : as a separator, not part of an annotation. C and C++: Labels, Ternary, and Scope In C, : is primarily a label terminator and the false-branch of ?:. int retry(int n) { int result = n > 0 ? n : -n; // ternary: condition ? then : else if (n == 0) goto done; goto retry; retry: result *= 2; done: return result; } C++ adds the scope-resolution operator ::, which is left-associative and binds tighter than any other operator except member access: #include namespace geo { struct Point { static constexpr int dims = 2; }; } int main() { int x = geo::Point::dims; // nested namespace + class scope using std::cout; cout > unary > *// > +/- > comparison > == > & > ^ > | > && > || > ?: > = > ,. Rust: Paths, Lifetimes, and Match Arms Rust uses :: as a path separator (like C++'s scope resolver) and : for match arms and struct-field shorthand. use std::collections::HashMap; fn describe(x: Option) -> &'static str { match x { Some(v) if v > 0 => "positive", Some(_) => "some", None => "none", } } fn main() { let mut m: HashMap = HashMap::new(); m.insert("a", 1); println!("{}", std::mem::size_of::()); // path + turbofish let _ = describe(Some(3)); } Paths crate::module::Item mirror filesystem layout; :: is left-associative. Match arms pattern => expr do not use : — Rust chose => to avoid ambiguity with struct fields. Struct literals use Field: value and require parentheses when inside an if/match condition (the classic "struct literal not allowed here" error). Lifetimes use 'a, an apostrophe, not a colon — a common point of confusion when reading C++/Java generics alongside Rust. TypeScript: Structural Types and Mapped Records TypeScript reuses : for both runtime object literals and static type positions, relying on context to disambiguate. type User = { name: string; // property: type age?: number; // optional marker before colon }; const u: User = { name: "Ada", age: 36 }; // variable: type = value function pick(k: T): User[T] { return u[k]; } const label = u.age! > 30 ? "senior" : "junior"; console.log(pick("name"), label); Key rules: In a type literal, name: string declares a property; in an object literal, name: "Ada" assigns a value. The parser tracks whether it is in a type position. Optional ? precedes the colon: age?: number (type) vs age?: 36 (value with exactOptionalPropertyTypes). Index signatures [key: string]: T and mapped types Record both rely on key: T syntax. Ternary cond ? a : b is right-associative and lower precedence than ||/&&. JavaScript: Objects, Labels, and the Missing Annotation JavaScript has no type annotations, so : appears mainly in object literals, labels, and destructuring defaults. const config = { host: "localhost", port: 8080, retries: 3 }; const { host, port = 3000 } = config; // default uses `=`, not `:` outer: for (const r of [1, 2]) { if (r === 2) break outer; } const mode = process.env.MODE ?? "dev"; console.log(config.retries > 0 ? mode : "none"); JSON inherits the object-literal form strictly: keys must be double-quoted strings, and : separates them from values. Trailing commas are illegal in JSON but legal in JS. Other Notable Uses Go uses : in short variable declarations (x := 5), type switches (case T:), and struct tags (`json:"name"`). Kotlin/Swift use name: Type for parameters and properties; Swift adds case .some(let x):. YAML/HCL treat : as a pure data delimiter; Terraform time literals like 12:30 sit inside strings to avoid ambiguity. Shell uses ${var:-default} and case ... in pat) — the colon there is parameter expansion, not a separate token. CSS uses property: value; and pseudo-selectors a:hover — the latter is a selector combinator, not an operator. Parsing and Precedence Pitfalls Most colon constructs are structural (produced by the grammar as distinct productions) rather than true operators with precedence. The exceptions — ?: and :: — do participate in precedence tables. Common mistakes: Misreading a ? b : c nesting. Because ?: is right-associative, x ? y ? 1 : 2 : 3 groups as x ? (y ? 1 : 2) : 3. Confusing :: with .. In C++, obj.Base::method() calls the base implementation explicitly; in Rust, Trait::method(x) is a UFCS path. Slicing off-by-one. Python's [a:b] excludes b; Lua's string.sub(s, a, b) includes b. TypeScript object vs type literal ambiguity in arrow functions: (x): T => ... needs parentheses or a return-type annotation to parse correctly. YAML's implicit typing. version: 1.20 may parse as a float; quote it ("1.20") to keep a string. When to Reach for Which Construct Need Prefer Why Annotate a type Language-native x: T Readable, tool-supported Pick between two values ?: (C-family) or if/else expression if/else wins for complex branches Disambiguate a name :: path or scope resolver Explicit, no ADL surprises Build a key/value map Dict/object literal Structural, hashable keys Extract a range Slice with a:b End-exclusive convention is portable For deeper language-design context, see tamiz.pro and Tamiz's Insights. Frequently Asked Questions Q: Why doesn't Python use ?: for conditionals? A: The colon is already heavily used for dict literals and slices. Adding ?: would create parsing ambiguity — {1: 2 if x else 3} would be hard to distinguish from a ternary in some contexts, so Python chose X if C else Y instead. Q: Is :: the same operator in C++ and Rust? A: Both resolve names in a scope, but C++'s :: participates in overload resolution, ADL, and can name template specializations (Foo::bar), while Rust's :: is purely a path separator in a module/trait hierarchy with no overloading. Q: How do I safely parse key: value in my own parser? A: Treat it as a grammar production, not an operator token. Lex : as a single token, then let the parser decide based on whether it expects a mapping entry, a type annotation, or a label — context-free grammar alternatives handle this cleanly.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to