PONY λ M2 Modula-2

C#.CodeCompared.To/Python

An interactive executable cheatsheet comparing C# and Python

C# 13 Python 3.13
Hello World & Output
Hello, World
Console.WriteLine("Hello, World!");
print("Hello, World!")
Python's print() is a built-in function — no using, no class, no Main. It adds a newline by default, just like Console.WriteLine. The entire program is that one line.
Running a program
// Compile and run with the .NET CLI: // dotnet run // Or run a single file directly (.NET 10+): // dotnet script file.cs
# Run directly — no compile step: # python3 hello.py # # One-liner without a file: # python3 -c 'print("Hi")' # # Interactive REPL: # python3
Python is interpreted — there is no compile step and no binary to ship. python3 file.py runs the script directly. The bare python3 command opens an interactive REPL for experimentation, which has no C# built-in equivalent (though dotnet-script offers something similar).
Formatted output
string language = "Python"; double version = 3.13; Console.WriteLine($"{language} {version:F2}"); Console.Write("no newline"); Console.WriteLine(" — then newline");
language = "Python" version = 3.13 print(f"{language} {version:.2f}") print("no newline", end="") print(" — then newline")
Python f-strings use the same {expression:format_spec} syntax as C# interpolated strings. :.2f in Python corresponds to :F2 in C#. print always adds a newline; pass end="" to suppress it, replacing Console.Write.
Comments
// Single-line comment int count = 0; /* Block comment spanning multiple lines */ Console.WriteLine(count);
# Single-line comment count = 0 # Python has no block comment syntax. # Convention: use consecutive # lines. print(count)
Python uses # for all comments. There is no block comment syntax — use a run of # lines for multi-line comments. Triple-quoted strings ("""...""") are sometimes used as multi-line comments but are technically string literals and do generate a bytecode object, so they are not true comments.
Variables & Types
Variable declaration
var greeting = "Hello"; // inferred: string int count = 0; // explicit type bool active = true; Console.WriteLine($"{greeting}, count={count}, active={active}");
greeting = "Hello" # no keyword, no type count = 0 active = True # True/False, not true/false print(f"{greeting}, count={count}, active={active}")
Python variables need no declaration keyword — just assign. There is no var, int, or string annotation (those come from type hints, which are optional). Boolean literals are True and False (capitalized), not true/false.
Dynamic typing
object value = 42; Console.WriteLine($"{value} is {value.GetType().Name}"); // C# variables are statically typed — can't reassign string to an int variable
value = 42 print(f"{value} is {type(value).__name__}") value = "now a string" # reassigning to a different type is fine print(f"{value} is {type(value).__name__}")
Python is dynamically typed — a variable holds any object and can be reassigned to a different type freely. type(x).__name__ is the runtime equivalent of x.GetType().Name. In C#, a variable's type is fixed at declaration; assigning a different type requires object or a cast.
Constants
const double PI = 3.14159; const string APP_NAME = "MyApp"; Console.WriteLine($"{APP_NAME}: Pi = {PI}");
PI = 3.14159 # ALL_CAPS = "treat as constant" APP_NAME = "MyApp" # nothing prevents reassignment print(f"{APP_NAME}: Pi = {PI}")
Python has no const keyword. The convention is to name module-level constants in ALL_CAPS; nothing in the language enforces immutability. For truly immutable values in a class, use a property with no setter. The Final annotation from typing signals intent to type checkers but is not enforced at runtime.
None vs null
string? name = null; Console.WriteLine(name?.Length ?? 0); Console.WriteLine(name == null);
name = None print(len(name) if name is not None else 0) print(name is None) # use 'is None', not '== None'
Python has one null-like value: None. The idiomatic check is x is None (identity), not x == None (equality), because a class could override __eq__. There is no ?. null-conditional operator; write an explicit if ... is not None guard. Python raises AttributeError on None.something, analogous to C#'s NullReferenceException.
Strings
String interpolation / f-strings
string city = "Paris"; int population = 2_161_000; Console.WriteLine($"{city} has {population:N0} people");
city = "Paris" population = 2_161_000 # underscores for readability — same as C# print(f"{city} has {population:,} people")
Python f-strings (f"...") mirror C# interpolated strings ($"..."). Format specs go after a colon inside the braces: {value:,.2f} in Python vs {value:N2} in C#. Both support arbitrary expressions in the braces, including method calls and ternary operators. Numeric literals can use underscores as digit separators in both languages.
String methods
string text = " Hello, World! "; Console.WriteLine(text.Trim().ToUpper()); Console.WriteLine(text.Trim().Replace("World", "Python")); Console.WriteLine("hello".Contains("ell"));
text = " Hello, World! " print(text.strip().upper()) print(text.strip().replace("World", "Python")) print("ello" in "hello") # 'in' operator instead of .Contains()
Python string methods are mostly lower_snake_case: strip/lstrip/rstrip for Trim/TrimStart/TrimEnd, upper/lower for ToUpper/ToLower, replace for Replace. Containment is checked with the in operator ("ell" in "hello") rather than a method call.
Split & join
string csv = "alpha,beta,gamma"; string[] parts = csv.Split(','); Console.WriteLine(string.Join(" | ", parts));
csv = "alpha,beta,gamma" parts = csv.split(",") print(" | ".join(parts))
The biggest API flip: in Python, join is a method on the separator string (sep.join(items)), while in C# it is a static method that takes the separator first (string.Join(sep, items)). Both split on a string delimiter and produce a list/array.
Multiline strings
string poem = """ Roses are red, Violets are blue. """; Console.WriteLine(poem.Trim());
poem = """ Roses are red, Violets are blue. """ print(poem.strip())
Both languages use triple-quoted strings for multiline literals. C# raw string literals ("""...""", C# 11+) strip the common indentation prefix automatically. Python's triple-quoted strings preserve leading whitespace exactly as written, so use .strip() or textwrap.dedent() to clean it up.
Numbers
Number types
int count = 42; long big = 10_000_000_000L; double ratio = 3.14; decimal price = 9.99m; Console.WriteLine($"{count} {big} {ratio} {price}");
count = 42 big = 10_000_000_000 # int is arbitrary precision — no overflow ratio = 3.14 # float is always 64-bit price = 9.99 # use 'decimal' module for exact decimal math print(count, big, ratio, price)
Python has two built-in numeric types: int (arbitrary precision — never overflows) and float (64-bit IEEE 754). There is no long, double, or decimal keyword. For exact decimal arithmetic import decimal.Decimal, which corresponds to C#'s decimal type.
Division semantics
Console.WriteLine(10 / 3); // 3 — integer division for int operands Console.WriteLine(10.0 / 3); // 3.333... — float when either is float Console.WriteLine(10 % 3); // 1 — remainder
print(10 / 3) # 3.333... — always float division print(10 // 3) # 3 — explicit floor division operator print(10 % 3) # 1 — same as C#
This is a key difference: in C#, 10 / 3 is 3 (integer division) when both operands are integers. In Python, / always returns a float — 10 / 3 is 3.3333.... Use // (floor division) for integer truncation in Python.
Math functions
Console.WriteLine(Math.Abs(-7)); Console.WriteLine(Math.Sqrt(16.0)); Console.WriteLine(Math.Round(3.567, 2)); Console.WriteLine(Math.Pow(2, 10));
import math print(abs(-7)) # built-in print(math.sqrt(16.0)) # math module print(round(3.567, 2)) # built-in print(2 ** 10) # ** is the power operator
Python's abs() and round() are built-in functions (no import). math.sqrt() and other transcendentals are in the math module. The ** operator replaces Math.Pow2 ** 10 is idiomatic Python where C# writes Math.Pow(2, 10).
Lists & Sequences
List basics
var numbers = new List<int> { 1, 2, 3 }; numbers.Add(4); Console.WriteLine(numbers.Count); Console.WriteLine(numbers[0]);
numbers = [1, 2, 3] numbers.append(4) print(len(numbers)) # len() instead of .Count print(numbers[0])
Python lists are equivalent to List<T> in C# but untyped — they can hold mixed types. append() corresponds to Add(), and the built-in len() replaces the .Count property. Index access is identical: items[0] for first, items[-1] for last (Python bonus).
List operations
var fruits = new List<string> { "apple", "banana", "cherry" }; fruits.Remove("banana"); fruits.Insert(0, "avocado"); Console.WriteLine(string.Join(", ", fruits));
fruits = ["apple", "banana", "cherry"] fruits.remove("banana") fruits.insert(0, "avocado") print(", ".join(fruits))
Common list operations map closely: remove(value) for Remove(value), insert(index, value) for Insert(index, value), pop() for RemoveAt(Count-1). Python also has extend(other_list) (like AddRange) and sort() which sorts in-place (while LINQ's OrderBy returns a new sequence).
Slicing
var numbers = new List<int> { 0, 1, 2, 3, 4, 5 }; var first3 = numbers.Take(3).ToList(); var last2 = numbers.TakeLast(2).ToList(); var middle = numbers.Skip(1).Take(3).ToList(); Console.WriteLine(string.Join(", ", middle));
numbers = [0, 1, 2, 3, 4, 5] first3 = numbers[:3] # same as Take(3) last2 = numbers[-2:] # same as TakeLast(2) middle = numbers[1:4] # same as Skip(1).Take(3) print(middle)
Python's slice syntax [start:stop:step] is a core language feature: [:3] for first 3, [-2:] for last 2, [1:4] for a middle range. Stop index is exclusive, matching Take(count) semantics. C# achieves the same with LINQ's Skip/Take/TakeLast or the ..^ range operator.
Tuples
(int Id, string Name) person = (1, "Alice"); Console.WriteLine($"{person.Id}: {person.Name}"); var (identifier, name) = person; // deconstruct Console.WriteLine(identifier);
person = (1, "Alice") print(f"{person[0]}: {person[1]}") identifier, name = person # unpack (deconstruct) print(identifier)
Python tuples are immutable sequences: (1, "Alice"). Elements are accessed by index (person[0]) unless you unpack them into named variables (id, name = person). C# value tuples support named fields (person.Id) and the same destructuring syntax. Both languages treat single-element tuples specially — in Python, a trailing comma is required: (42,).
Dicts & Maps
Dict basics
var scores = new Dictionary<string, int> { ["Alice"] = 95, ["Bob"] = 87, }; Console.WriteLine(scores["Alice"]); Console.WriteLine(scores.Count);
scores = {"Alice": 95, "Bob": 87} print(scores["Alice"]) print(len(scores))
Python dicts use {"key": value} literal syntax. Access is identical (dict["key"]), and missing keys raise KeyError just as C# raises KeyNotFoundException. len(dict) replaces .Count. Python dicts preserve insertion order (guaranteed since Python 3.7), matching C#'s Dictionary behavior from .NET 5+.
Safe access & defaults
var config = new Dictionary<string, string> { ["theme"] = "dark" }; string mode = config.GetValueOrDefault("mode", "light"); bool hasTheme = config.ContainsKey("theme"); Console.WriteLine($"{mode}, hasTheme={hasTheme}");
config = {"theme": "dark"} mode = config.get("mode", "light") # .get() with default has_theme = "theme" in config # 'in' operator print(f"{mode}, has_theme={has_theme}")
dict.get(key, default) returns the default if the key is absent, mirroring GetValueOrDefault. Membership is checked with "key" in dict (the in operator), not a ContainsKey method call. Python also has dict.setdefault(key, value) which inserts and returns the default if the key is absent — handy for building nested structures.
Dict iteration
var capitals = new Dictionary<string, string> { ["France"] = "Paris", ["Japan"] = "Tokyo", }; foreach (var (country, capital) in capitals) Console.WriteLine($"{country}: {capital}");
capitals = {"France": "Paris", "Japan": "Tokyo"} for country, capital in capitals.items(): print(f"{country}: {capital}")
Iterating a Python dict by default yields keys only (like C#'s foreach (var k in dict)). Use dict.items() to get key-value pairs, dict.keys() for just keys, and dict.values() for just values — mirroring C#'s .Keys and .Values properties.
Merging dicts
var defaults = new Dictionary<string, int> { ["timeout"] = 30, ["retries"] = 3 }; var overrides = new Dictionary<string, int> { ["timeout"] = 60 }; var merged = defaults .Concat(overrides) .GroupBy(kv => kv.Key) .ToDictionary(group => group.Key, group => group.Last().Value); Console.WriteLine(merged["timeout"]);
defaults = {"timeout": 30, "retries": 3} overrides = {"timeout": 60} merged = defaults | overrides # Python 3.9+ merge operator print(merged["timeout"]) # overrides win: 60
Python 3.9 introduced the | merge operator for dicts — the right operand wins on key conflicts. The in-place version (defaults |= overrides) updates in place. Before Python 3.9, the idiom was {**defaults, **overrides}. C# has no single built-in dict-merge operator; the common approach concatenates with LINQ and resolves duplicates.
Control Flow
If / else
int temperature = 25; if (temperature > 30) Console.WriteLine("hot"); else if (temperature > 20) Console.WriteLine("warm"); else Console.WriteLine("cool");
temperature = 25 if temperature > 30: print("hot") elif temperature > 20: # 'elif', not 'else if' print("warm") else: print("cool")
Python uses significant indentation to define blocks (no braces), a colon after each header line, and elif (not else if) for chained conditions. Parentheses around the condition are not needed. Boolean operators are spelled out: and, or, not instead of &&, ||, !.
For loop
for (int i = 0; i < 5; i++) Console.Write(i + " "); Console.WriteLine();
for i in range(5): # range() produces 0..4 print(i, end=" ") print()
Python has no C-style for (init; condition; step) loop. Use range(stop) for a count, range(start, stop) for a range, and range(start, stop, step) for a step. range objects are lazy — they do not create a list in memory. print(end="") suppresses the newline, mimicking Console.Write.
foreach / for-in
var fruits = new[] { "apple", "banana", "cherry" }; foreach (var fruit in fruits) Console.WriteLine(fruit);
fruits = ["apple", "banana", "cherry"] for fruit in fruits: print(fruit)
Python's for item in iterable directly mirrors C#'s foreach. enumerate(items) gives index-value pairs like items.Select((item, i) => (i, item)): for i, fruit in enumerate(fruits). Any iterable — list, string, dict, file, generator — works with for.
While loop
int counter = 3; while (counter > 0) { Console.WriteLine(counter); counter--; }
counter = 3 while counter > 0: print(counter) counter -= 1 # no -- operator in Python
Python's while loop is syntactically identical to C#'s except for the colon and indentation. Python has no do-while loop — use while True: ... if condition: break to emulate one. Python also has no ++ or -- operators; use += 1 / -= 1.
Switch / match
int code = 200; string status = code switch { 200 => "OK", 404 => "Not Found", 500 => "Server Error", _ => "Unknown", }; Console.WriteLine(status);
code = 200 match code: case 200: status = "OK" case 404: status = "Not Found" case 500: status = "Server Error" case _: status = "Unknown" print(status)
Python's match statement (added in Python 3.10) matches the C# switch expression concept. Both support a wildcard (_) default arm. Python's structural pattern matching goes further than C#'s switch — it can destructure sequences, dicts, and class instances in patterns, similar to C# 11+ pattern matching but as a first-class statement.
Functions
Function definition
Console.WriteLine(Add(3, 4)); Console.WriteLine(Multiply(3, 4)); static int Add(int a, int b) => a + b; static int Multiply(int a, int b) { return a * b; }
def add(a, b): return a + b def multiply(a, b): return a * b # no expression-body form; always use 'def' print(add(3, 4)) print(multiply(3, 4))
Python functions are defined with def name(params): followed by an indented body. There is no return-type annotation unless you add type hints. Functions can be defined at module level, nested inside other functions, or as class methods. Python has no arrow-expression equivalent (=> expr) for functions — that concise single-expression form is only available as lambda.
Default & keyword arguments
Console.WriteLine(Greet("Alice")); Console.WriteLine(Greet("Bob", greeting: "Hi")); static string Greet(string name, string greeting = "Hello") => $"{greeting}, {name}!";
def greet(name, greeting="Hello"): return f"{greeting}, {name}!" print(greet("Alice")) print(greet("Bob", greeting="Hi")) # keyword argument
Python default parameters work identically to C#'s optional parameters. But Python also supports keyword arguments at any call site — any parameter can be passed by name regardless of position (greet("Bob", greeting="Hi")), unlike C# where you must match position or use the named syntax explicitly. This makes Python's function calls far more self-documenting.
*args and **kwargs
Display("scores", 95, 87, 72); static void Display(string label, params int[] values) => Console.WriteLine($"{label}: {string.Join(", ", values)}");
def display(label, *values): # *args collects positional extras print(f"{label}: {', '.join(str(v) for v in values)}") def configure(**options): # **kwargs collects keyword extras for key, value in options.items(): print(f" {key} = {value}") display("scores", 95, 87, 72) configure(timeout=30, retries=3)
*args collects extra positional arguments into a tuple — the equivalent of C#'s params T[]. **kwargs collects extra keyword arguments into a dict — there is no C# equivalent (the closest pattern is an IDictionary<string, object> parameter or an anonymous type). A function can combine both: def func(*args, **kwargs).
Lambdas
Func<int, int> square = x => x * x; Func<int, int, int> add = (a, b) => a + b; Console.WriteLine(square(5)); Console.WriteLine(add(3, 4));
square = lambda x: x * x add = lambda a, b: a + b print(square(5)) print(add(3, 4))
Python's lambda x: expression mirrors C#'s x => expression for simple single-expression functions. The key difference: Python lambdas can only contain a single expression (no statements, no return). For multi-line logic, define a full def. In practice, Python prefers named def functions over lambdas for anything beyond a sort key or quick map/filter argument.
Higher-order functions
var numbers = new[] { 1, -2, 3, -4, 5 }; var result = numbers .Where(n => n > 0) .Select(n => n * 2) .ToList(); Console.WriteLine(string.Join(", ", result));
numbers = [1, -2, 3, -4, 5] # Prefer comprehensions over map/filter in Python: result = [n * 2 for n in numbers if n > 0] print(", ".join(str(n) for n in result))
Python has map(func, iterable) and filter(func, iterable) built-ins, but the idiomatic Python style is to use list comprehensions ([expr for x in seq if cond]) instead. They are more readable than chained lambdas and execute in a single pass. LINQ's fluent chaining corresponds directly to comprehension conditions: .Where(pred)if pred inside the comprehension.
Comprehensions & LINQ
List comprehension vs LINQ Select
var squares = Enumerable.Range(1, 5) .Select(n => n * n) .ToList(); Console.WriteLine(string.Join(", ", squares));
squares = [n * n for n in range(1, 6)] print(", ".join(str(n) for n in squares))
A Python list comprehension replaces LINQ's Select(...).ToList(). The syntax is [expression for variable in iterable]. Range is range(1, 6) (stop is exclusive), matching Enumerable.Range(1, 5). Comprehensions are faster than equivalent map()/list() calls in CPython because they avoid function-call overhead per element.
Filtering vs LINQ Where
var numbers = Enumerable.Range(1, 10); var evens = numbers .Where(n => n % 2 == 0) .ToList(); Console.WriteLine(string.Join(", ", evens));
numbers = range(1, 11) evens = [n for n in numbers if n % 2 == 0] print(", ".join(str(n) for n in evens))
Adding an if clause to a comprehension replaces LINQ's .Where(). Combined select+filter (Where(...).Select(...)) maps naturally: [expr for x in seq if condition]. Nested comprehensions replace SelectMany: [item for sub in nested for item in sub].
Generator expressions
var numbers = Enumerable.Range(1, 100); int sumSquares = numbers.Sum(n => n * n); Console.WriteLine(sumSquares);
numbers = range(1, 101) sum_squares = sum(n * n for n in numbers) # generator expression — no [] print(sum_squares)
A generator expression looks like a list comprehension but without the outer brackets. It produces values lazily one at a time — no intermediate list is allocated. This is the Python equivalent of LINQ's deferred execution. sum(n*n for n in numbers) is more memory-efficient than sum([n*n for n in numbers]) and is the idiomatic Python for aggregation.
Classes & OOP
Class basics
var cat = new Animal("Whiskers"); Console.WriteLine(cat.Speak()); class Animal { public string Name { get; } public Animal(string name) { Name = name; } public string Speak() => $"{Name} says hello!"; }
class Animal: def __init__(self, name): # constructor self.name = name # instance attribute def speak(self): # 'self' is always explicit return f"{self.name} says hello!" cat = Animal("Whiskers") print(cat.speak())
Python classes use __init__ as the constructor (the self parameter is the instance and must be declared explicitly on every method). There are no access modifiers: attributes are public by default, and a leading underscore (_name) signals "private by convention". Properties require a separate decorator — the next concept.
Inheritance
var dog = new Dog("Rex", "Labrador"); Console.WriteLine(dog.Describe()); class Animal { public string Name { get; } public Animal(string name) { Name = name; } } class Dog : Animal { public string Breed { get; } public Dog(string name, string breed) : base(name) { Breed = breed; } public string Describe() => $"{Name} ({Breed})"; }
class Animal: def __init__(self, name): self.name = name class Dog(Animal): def __init__(self, name, breed): super().__init__(name) # call parent constructor self.breed = breed def describe(self): return f"{self.name} ({self.breed})" dog = Dog("Rex", "Labrador") print(dog.describe())
Inheritance syntax: class Dog(Animal) vs C#'s class Dog : Animal. Call the parent constructor with super().__init__(...) instead of C#'s : base(...). Python supports multiple inheritance (class C(A, B)); C# allows only single class inheritance. Method overriding requires no override keyword — just redefine the method.
Properties
var circle = new Circle(5.0); Console.WriteLine($"r={circle.Radius:F1}, area={circle.Area:F4}"); class Circle { public double Radius { get; } public Circle(double radius) { Radius = radius; } public double Area => Math.PI * Radius * Radius; }
import math class Circle: def __init__(self, radius): self._radius = radius # _prefix = private by convention @property def radius(self): return self._radius # getter @property def area(self): return math.pi * self._radius ** 2 # computed property circle = Circle(5.0) print(f"r={circle.radius:.1f}, area={circle.area:.4f}")
Python uses the @property decorator to create read-only computed properties that look like attribute access. To add a setter, define a second method decorated with @name.setter. This corresponds directly to C#'s { get; } and { get; set; } property syntax. Store the backing value in an underscore-prefixed attribute (self._radius) by convention.
Dataclass vs record
var point = new Point(3.0, 4.0); Console.WriteLine($"({point.X}, {point.Y})"); Console.WriteLine(point == new Point(3.0, 4.0)); // true — value equality record Point(double X, double Y);
from dataclasses import dataclass @dataclass class Point: x: float y: float point = Point(3.0, 4.0) print(f"({point.x}, {point.y})") print(point == Point(3.0, 4.0)) # True — value equality
@dataclass is the closest Python analog to a C# record: it auto-generates __init__, __repr__, and __eq__ from the field annotations. Add frozen=True to make it immutable (like a record with no setters). The dataclasses.field() function provides the same control as C# property initializers.
Operator overloading / dunder methods
var vector = new Vector(1, 2); Console.WriteLine(vector); Console.WriteLine(vector + new Vector(3, 4)); class Vector { public int X, Y; public Vector(int x, int y) { X = x; Y = y; } public override string ToString() => $"({X}, {Y})"; public static Vector operator +(Vector a, Vector b) => new Vector(a.X + b.X, a.Y + b.Y); }
class Vector: def __init__(self, x, y): self.x, self.y = x, y def __repr__(self): # like ToString() return f"({self.x}, {self.y})" def __add__(self, other): # like operator+ return Vector(self.x + other.x, self.y + other.y) vector = Vector(1, 2) print(vector) print(vector + Vector(3, 4))
Python's "dunder" (double-underscore) methods replace C#'s override + operator declarations. __repr__ corresponds to ToString(), __add__ to operator +, __len__ to a Count property, and so on. Unlike C#, Python does not require a companion __radd__ if the left type handles addition.
Error Handling
try / except
try { int result = int.Parse("abc"); Console.WriteLine(result); } catch (FormatException exception) { Console.WriteLine($"Caught: {exception.Message}"); }
try: result = int("abc") print(result) except ValueError as error: print(f"Caught: {error}")
Python uses except instead of catch, and the exception variable is bound with as: except ValueError as error. Python's exception hierarchy uses ValueError for parse failures (C#'s FormatException), TypeError for wrong types (C#'s InvalidCastException), and AttributeError for missing members (C#'s NullReferenceException).
Multiple exception types
try { var items = new int[] { 1, 2, 3 }; Console.WriteLine(items[10]); } catch (IndexOutOfRangeException) { Console.WriteLine("Index out of range"); } catch (Exception exception) { Console.WriteLine($"Other: {exception.Message}"); }
try: items = [1, 2, 3] print(items[10]) except IndexError: print("Index out of range") except Exception as error: print(f"Other: {error}")
Multiple except clauses work like multiple catch blocks — first match wins. To catch several exceptions in one clause: except (ValueError, TypeError) as error. Python's base class for all user-catchable exceptions is Exception; BaseException also catches KeyboardInterrupt and SystemExit, which is rarely desired.
Custom exceptions
try { Validate(-5); } catch (AppException exception) { Console.WriteLine(exception.Message); } static void Validate(int value) { if (value < 0) throw new AppException($"Value {value} must be non-negative"); } class AppException(string message) : Exception(message);
class AppException(Exception): pass # inherit everything from Exception def validate(value): if value < 0: raise AppException(f"Value {value} must be non-negative") try: validate(-5) except AppException as error: print(error)
Custom exceptions inherit from Exception (or a more specific subclass). A bare pass body is all that is needed if you add no new behavior — the message is set by the base Exception.__init__. The keyword is raise (not throw). Re-raising the current exception uses bare raise with no argument, just like C#'s bare throw.
using / with
using var reader = new StringReader("line one\nline two"); Console.WriteLine(reader.ReadLine()); // reader is disposed automatically when the block exits
import io with io.StringIO("line one\nline two") as reader: print(reader.readline()) # reader.__exit__ is called automatically here
Python's with statement is the direct equivalent of C#'s using declaration or using (...) {} block. Any object that implements __enter__ and __exit__ (a "context manager") can be used with with. This covers file handles, locks, database connections, and network sockets — exactly the same resources that C#'s IDisposable covers.
Type Hints
Variable type hints
int count = 42; string name = "Alice"; bool active = true; Console.WriteLine($"{name}: {count}, {active}");
count: int = 42 name: str = "Alice" active: bool = True print(f"{name}: {count}, {active}")
Python type hints (name: type) look like C# explicit declarations but have no runtime effect — the interpreter ignores them. They are checked by external tools (mypy, pyright) or IDEs. Omitting them is perfectly valid Python. Think of them as structured comments that enable IDE autocomplete and static analysis, rather than compile-time enforcement.
Function return types
Console.WriteLine(Greet("Alice")); static string Greet(string name) => $"Hello, {name}!";
def greet(name: str) -> str: return f"Hello, {name}!" print(greet("Alice"))
Function type hints use param: type for parameters and -> type for the return type — the arrow mirrors C#'s method signature layout. A function with no return statement returns None, and its hint should be -> None (equivalent to C#'s void). Passing the wrong type raises no runtime error unless you run a type checker separately.
Collection & optional types
List<string> names = ["Alice", "Bob"]; Dictionary<string, int> scores = new() { ["Alice"] = 95 }; string? nickname = null; Console.WriteLine(names.Count);
names: list[str] = ["Alice", "Bob"] # Python 3.9+ scores: dict[str, int] = {"Alice": 95} nickname: str | None = None # Python 3.10+ union syntax print(len(names))
Python 3.9+ accepts built-in types as generic hints: list[str] instead of the older List[str] from typing. The | union syntax (str | None) was added in Python 3.10; before that you'd write Optional[str] from typing. All of this is still advisory — no runtime enforcement.
Async & Generators
async / await
await SayHelloAsync(); async Task SayHelloAsync() { await Task.Delay(1); Console.WriteLine("Hello from async!"); }
import asyncio async def say_hello(): await asyncio.sleep(0) # equivalent to Task.Delay(1) print("Hello from async!") asyncio.run(say_hello())
Python uses the same async def / await keywords as C#. The key difference is the runtime: C# uses a multi-threaded thread pool; Python uses a single-threaded event loop (asyncio) with the GIL. Start the event loop with asyncio.run(coroutine) — the equivalent of C#'s top-level await in a console program. Async functions return a coroutine, not a Task.
Concurrent tasks
var results = await Task.WhenAll(FetchAsync("A"), FetchAsync("B")); Console.WriteLine(string.Join(", ", results)); async Task<string> FetchAsync(string name) { await Task.Delay(1); return $"data from {name}"; }
import asyncio async def fetch(name): await asyncio.sleep(0) return f"data from {name}" async def main(): results = await asyncio.gather(fetch("A"), fetch("B")) print(", ".join(results)) asyncio.run(main())
asyncio.gather() is the Python equivalent of Task.WhenAll() — it runs multiple coroutines concurrently and collects their results in order. Results come back as a list. For cancellation or first-completed semantics, see asyncio.wait() (like Task.WhenAny). Note that asyncio concurrency is cooperative, not parallel — it does not bypass the GIL for CPU-bound work.
Generators vs yield return
foreach (var n in EvenNumbers(5)) Console.Write(n + " "); Console.WriteLine(); static IEnumerable<int> EvenNumbers(int count) { for (int i = 0; i < count; i++) yield return i * 2; }
def even_numbers(count): for i in range(count): yield i * 2 # 'yield' makes this a generator function for n in even_numbers(5): print(n, end=" ") print()
A Python function containing yield becomes a generator function — exactly like C#'s IEnumerable<T> method with yield return. Both are lazy: values are produced on demand as the caller iterates. Python also has yield from sub_generator to delegate to another generator, and generator expressions ((expr for x in seq)) for inline lazy sequences.
Gotchas
Truthiness
var items = new List<int>(); // C# requires an explicit boolean expression: if (items.Count == 0) Console.WriteLine("empty list"); if (string.IsNullOrEmpty("")) Console.WriteLine("empty string");
items = [] if not items: # empty list is falsy print("empty list") if not "": # empty string is falsy print("empty string") # All of these are falsy in Python: for value in [0, 0.0, "", [], {}, None, False]: if not value: print(f"{value!r} is falsy")
Python treats empty containers ([], {}, ""), zero (0, 0.0), and None as falsy in a boolean context. This is convenient — if items: means "if the list is non-empty" — but can surprise C# developers who expect only an explicit bool. Objects can define their own truthiness via __bool__ or __len__.
"is" checks identity, not equality
string text = "hello"; Console.WriteLine(text == "hello"); // true — value equality Console.WriteLine(object.ReferenceEquals(text, "hello")); // maybe true (interning) int? value = null; Console.WriteLine(value == null); // the normal null check
text = "hello" print(text == "hello") # True — value equality; use == for values print(text is "hello") # True only by accident (string interning) value = None print(value is None) # correct — always use 'is None' print(value == None) # works but misleading — avoid
In Python, is checks object identity (like Object.ReferenceEquals), while == checks value equality (like a normal == in C#). The critical exception: always use is None / is not None for null checks, not == None, because a class can override __eq__ to return True when compared to None. Never use is to compare strings or numbers.
Mutable default arguments
// C#: default values are always freshly evaluated per call Console.WriteLine(string.Join(",", Collect("a"))); Console.WriteLine(string.Join(",", Collect("b"))); static List<string> Collect(string item, List<string>? accumulator = null) { accumulator ??= []; accumulator.Add(item); return accumulator; }
# The list is created ONCE at function definition, not per call: def collect_bad(item, result=[]): result.append(item) return result print(collect_bad("a")) # ['a'] print(collect_bad("b")) # ['a', 'b'] — shared state! # Correct pattern — use None as sentinel: def collect(item, result=None): if result is None: result = [] result.append(item) return result
One of the most notorious Python gotchas: default argument values are evaluated once when the function is defined, not on each call. A mutable default (list, dict, set) is shared across all calls. The standard fix: default to None and create a fresh object inside the function body when None is received. C# always evaluates defaults at the call site, so this problem does not arise.
No method overloading
// C#: real overloading — the compiler picks the right version: Console.WriteLine(Formatter.Describe(42)); Console.WriteLine(Formatter.Describe("hello")); static class Formatter { public static string Describe(int value) => $"int: {value}"; public static string Describe(string value) => $"str: {value}"; }
# Python: the last definition wins — earlier ones are overwritten: def describe(value): return f"int: {value}" def describe(value): # silently replaces the first! return f"str: {value}" print(describe(42)) # "str: 42" — uses the second one print(describe("hello")) # "str: hello"
Python has no method overloading. Defining two functions with the same name in the same scope silently replaces the first — no error, no warning. Instead, use default arguments, *args/**kwargs, or check the type with isinstance() inside a single function. The functools.singledispatch decorator provides type-based dispatch as a library feature.