You already know variables, loops, classes, and HTTP. Python needs a translation layer, not a restart. You swap braces for indentation, static declarations for dynamic typing with optional hints, and verbose boilerplate for built-in data structures and comprehensions. This guide maps what you know to idiomatic Python, flags the traps that catch experienced developers, and shows refactors from “Java-in-Python” to Pythonic code.
Quick Takeaways
- Indentation is syntax. Four spaces define blocks. No braces, no semicolons.
- Everything is an object, and names are references. Variables are labels, not typed containers.
- Use built-ins first. list, dict, set, tuple, comprehensions, and the standard library replace most boilerplate.
- Type hints are optional and unenforced at runtime. Tools like mypy and pyright check them statically.
| Concept | Java | C# | JavaScript | Python |
|---|---|---|---|---|
| Typing | Static | Static | Dynamic | Dynamic (+ hints) |
| Block delimiter | {} |
{} |
{} |
Indentation |
| Null value | null |
null |
null/undefined |
None |
| Package manager | Maven/Gradle | NuGet | npm | pip / uv |
| Interface | interface |
interface |
Duck typing | Protocol / ABC |
| Async | CompletableFuture |
async/await |
async/await |
asyncio |
Setting Up: Environment and Tooling
Python projects isolate dependencies with virtual environments. This replaces the per-project node_modules or Maven local repository.
# Create and activate a virtual environment
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# Install packages
pip install requests pytest
# Freeze dependencies
pip freeze > requirements.txt
The venv keeps packages local to the project. Modern tools such as uv and poetry add lockfiles and faster resolution, similar to package-lock.json.
Syntax Breakdown: Variables, Types, and Control Flow
Variables and Dynamic Typing
No int x = 5;. You assign, and the name binds to an object.
count = 5 # int
count = "five" # legal: the name now references a str
price: float = 9.99 # optional type hint, not enforced at runtime
The second line is legal because dynamic typing checks types at runtime. Hints document intent and enable static analysis.
Core Types Mapped
| Python | Closest Java | Closest C# | Closest JS |
|---|---|---|---|
| int (unbounded) | BigInteger |
BigInteger |
BigInt |
| float | double |
double |
Number |
| str (immutable) | String |
string |
string |
| list | ArrayList |
List<T> |
Array |
| dict | HashMap |
Dictionary |
Map/object |
| set | HashSet |
HashSet<T> |
Set |
| tuple | (record-ish) | ValueTuple |
frozen array |
Python int has arbitrary precision. No overflow.
Control Flow
# if / elif / else (not "else if")
temperature = 72
if temperature > 85:
label = "hot"
elif temperature > 60:
label = "mild"
else:
label = "cold"
# for iterates over any iterable, not an index counter
for name in ["Ada", "Linus", "Guido"]:
print(name)
# Need the index? Use enumerate()
for i, name in enumerate(["Ada", "Linus"], start=1):
print(i, name)
# while works as expected
n = 3
while n > 0:
n -= 1
The output prints each name, then numbered names. Python has no ++ operator and no C-style for (i=0; i<n; i++). Use range() for numeric loops: for i in range(10).
Truthiness
Python treats empty collections, 0, "", and None as falsy.
items = []
if not items: # idiomatic; no len(items) == 0
print("empty")
This prints empty. Compare to None with is, never ==: if value is None.
Functions: First-Class, Flexible, Keyword-Friendly
def greet(name: str, greeting: str = "Hello", *, excited: bool = False) -> str:
"""Return a greeting. Keyword-only flag follows the bare *."""
suffix = "!" if excited else "."
return f"{greeting}, {name}{suffix}"
print(greet("Ada")) # Hello, Ada.
print(greet("Ada", greeting="Hi", excited=True)) # Hi, Ada!
Key differences from what you know:
- Default arguments replace method overloading.
- Keyword arguments make call sites self-documenting.
- f-strings replace
String.format,$""interpolation, and template literals. - *args and **kwargs capture variable positional and keyword arguments, like JS rest parameters.
Multiple return values use tuples and unpacking:
def min_max(values: list[int]) -> tuple[int, int]:
return min(values), max(values)
low, high = min_max([4, 9, 1, 7]) # low=1, high=9
Mutable Default Argument Trap
def append_item(item, bucket=[]): # BAD: evaluated once at definition
bucket.append(item)
return bucket
print(append_item(1)) # [1]
print(append_item(2)) # [1, 2] <- surprise
Defaults evaluate once, at function definition. Use None as a sentinel:
def append_item(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
Data Structures and Comprehensions
Lists, Dicts, Sets
nums = [3, 1, 4, 1, 5]
nums.append(9) # O(1) amortized
nums.sort() # in-place, O(n log n) (Timsort)
top = sorted(nums, reverse=True)[:3] # new list; original untouched
user = {"name": "Ada", "role": "admin"}
role = user.get("role", "guest") # safe lookup with default
unique = set(nums) # membership test is O(1) average
Lookup in a dict or set averages O(1). Membership in a list is O(n). That difference drives many performance fixes.
Comprehensions
Comprehensions replace stream().map().filter().collect(), LINQ Select/Where, and .map().filter().
squares_of_evens = [n * n for n in range(10) if n % 2 == 0]
# [0, 4, 16, 36, 64]
name_to_len = {name: len(name) for name in ["Ada", "Linus"]}
# {'Ada': 3, 'Linus': 5}
unique_lengths = {len(name) for name in ["Ada", "Bob", "Linus"]}
# {3, 5}
Use generator expressions when you don’t need the full list in memory:
total = sum(n * n for n in range(1_000_000)) # lazy, O(1) memory
Slicing
data = [10, 20, 30, 40, 50]
data[1:3] # [20, 30]
data[:2] # [10, 20]
data[-2:] # [40, 50]
data[::-1] # [50, 40, 30, 20, 10]
Slices are half-open: start inclusive, stop exclusive. Negative indexes count from the end.
Object-Oriented Python
Classes Without Boilerplate
class Account:
"""Simple bank account."""
def __init__(self, owner: str, balance: float = 0.0) -> None:
self.owner = owner # public by convention
self._balance = balance # single underscore: "internal"
@property
def balance(self) -> float:
return self._balance
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("amount must be positive")
self._balance += amount
acct = Account("Ada")
acct.deposit(50)
print(acct.balance) # 50.0
Differences to internalize:
- self is explicit. It is the first parameter of every instance method.
- __init__ is the initializer.
__new__allocates, but you rarely touch it. - No access modifiers. A leading underscore signals “internal.” A double underscore triggers name mangling, not true privacy.
- @property replaces getter/setter pairs.
Dataclasses Replace POJOs, Records, and DTOs
from dataclasses import dataclass, field
@dataclass(frozen=True)
class Point:
x: float
y: float
@dataclass
class Order:
id: int
items: list[str] = field(default_factory=list)
p = Point(1.0, 2.0)
print(p) # Point(x=1.0, y=2.0)
print(p == Point(1, 2)) # True
@dataclass generates __init__, __repr__, and __eq__. Setting frozen=True makes instances immutable and hashable. Use default_factory for mutable defaults.
Inheritance, Duck Typing, and Protocols
from typing import Protocol
class Notifier(Protocol):
def send(self, message: str) -> None: ...
class EmailNotifier:
def send(self, message: str) -> None:
print(f"Email: {message}")
def alert(notifier: Notifier, msg: str) -> None:
notifier.send(msg)
alert(EmailNotifier(), "Disk full") # Email: Disk full
EmailNotifier never declares implements Notifier. It satisfies the Protocol structurally. This is static duck typing, similar to TypeScript interfaces. For nominal contracts, use abc.ABC with @abstractmethod.
Python supports multiple inheritance, resolved by the MRO (method resolution order, C3 linearization). Prefer composition and mixins used sparingly.
Dunder Methods
| Dunder | Equivalent in Java/C#/JS |
|---|---|
__str__ |
toString() / ToString() |
__eq__ / __hash__ |
equals() + hashCode() |
__len__ |
size() / Count |
__iter__ |
Iterable / IEnumerable |
__enter__ / __exit__ |
try-with-resources / using |
__call__ |
functor / delegate |
Error Handling and Resource Management
from pathlib import Path
def read_config(path: str) -> dict[str, str]:
config: dict[str, str] = {}
try:
with Path(path).open(encoding="utf-8") as f: # auto-closes the file
for line in f:
key, _, value = line.strip().partition("=")
if key:
config[key] = value
except FileNotFoundError:
print(f"Missing config: {path}")
except PermissionError as exc:
raise RuntimeError("Cannot read config") from exc
else:
print("Config loaded")
finally:
print("Done")
return config
The with statement is Python’s try-with-resources and using. The else clause runs only when no exception occurred. Use raise ... from exc to preserve the cause chain.
Python favors EAFP (“easier to ask forgiveness than permission”): try the operation, catch the failure. Java developers often write LBYL (“look before you leap”) checks that add noise.
Modules, Packages, and Imports
# project layout
# myapp/
# __init__.py
# models.py
# services/
# __init__.py
# billing.py
from myapp.models import Order
from myapp.services import billing
import json
Each .py file is a module. A directory with __init__.py is a package. Guard script entry points:
def main() -> None:
print("running")
if __name__ == "__main__":
main()
The guard prevents main() from running when another module imports the file.
Concurrency: GIL, Threads, and asyncio
CPython has a Global Interpreter Lock (GIL) that lets one thread execute Python bytecode at a time in the default build. Recent CPython releases include an experimental free-threaded build, but standard builds still have the GIL. Choose the tool by workload:
| Workload | Best Tool | Why |
|---|---|---|
| I/O-bound (HTTP, DB) | asyncio / threads | Waiting releases the GIL |
| CPU-bound | multiprocessing / ProcessPoolExecutor |
Separate processes bypass the GIL |
| Simple parallel map | concurrent.futures | High-level API |
import asyncio
async def fetch(name: str, delay: float) -> str:
await asyncio.sleep(delay) # non-blocking wait
return f"{name} done"
async def main() -> None:
results = await asyncio.gather(fetch("A", 1), fetch("B", 1))
print(results) # ['A done', 'B done'] after ~1s total
asyncio.run(main())
Both tasks wait concurrently, so total runtime is about 1 second, not 2. If you know JS or C# async/await, the model transfers directly. asyncio.run() is the event loop entry point.
Bad Code vs. Good Code: Refactoring Java-isms
Anti-pattern 1: Index Loops
# BAD: C-style iteration
names = ["Ada", "Linus", "Guido"]
for i in range(len(names)):
print(names[i])
# GOOD: iterate directly
for name in names:
print(name)
Anti-pattern 2: Manual Accumulation
# BAD
result = []
for n in range(10):
if n % 2 == 0:
result.append(n * n)
# GOOD: comprehension
result = [n * n for n in range(10) if n % 2 == 0]
Anti-pattern 3: Getters and Setters
# BAD: Java-style accessors
class User:
def __init__(self, name):
self._name = name
def get_name(self):
return self._name
def set_name(self, name):
self._name = name
# GOOD: plain attribute; add @property later only if logic is needed
class User:
def __init__(self, name: str) -> None:
self.name = name
Python lets you convert a public attribute to a @property later without breaking callers.
Anti-pattern 4: Bare Except and Manual Cleanup
# BAD
f = open("data.txt")
try:
data = f.read()
except: # swallows KeyboardInterrupt and everything else
data = None
finally:
f.close()
# GOOD
try:
with open("data.txt", encoding="utf-8") as f:
data = f.read()
except OSError:
data = None
Anti-pattern 5: String Concatenation in Loops
# BAD: O(n^2) in the worst case
out = ""
for word in words:
out += word + " "
# GOOD: O(n)
out = " ".join(words)
Common Gotchas for Each Background
| Coming From | Gotcha | Python Reality |
|---|---|---|
| Java | Checked exceptions, getX() habits |
No checked exceptions; use attributes |
| Java | == vs .equals() |
== compares values; is compares identity |
| C# | var is still static |
Python names have no declared type |
| C# | LINQ chains | Prefer comprehensions and itertools |
| JS | === vs == |
== is already strict about types (no coercion) |
| JS | this binding quirks |
self is explicit and never rebinds |
| All | Block scope | Python has function scope; if/for blocks don’t create scope |
Tooling Cheat Sheet
| Task | Tool |
|---|---|
| Testing | pytest |
| Formatting | black or ruff format |
| Linting | ruff / flake8 |
| Static typing | mypy / pyright |
| Dependency management | uv / poetry / pip |
| Web frameworks | FastAPI, Django, Flask |
Write your first test now:
# test_math.py
def add(a: int, b: int) -> int:
return a + b
def test_add():
assert add(2, 3) == 5
Run pytest. It discovers test_*.py files and test_* functions automatically, with no annotations or runners to configure.
A 7-Day Fast Track Plan
- Day 1: Install Python, create a venv, solve five small problems using built-in types.
- Day 2: Master comprehensions, slicing, and unpacking.
- Day 3: Write functions with default, keyword, and variadic arguments.
- Day 4: Build classes, dataclasses, and a Protocol.
- Day 5: Handle files and exceptions with with and pathlib.
- Day 6: Add type hints and run mypy; write pytest tests.
- Day 7: Build a small CLI or FastAPI service using asyncio and requests/httpx.
FAQ
Is Python hard to learn if I already know Java or C#?
No. Syntax and idioms take about a week to absorb. Your knowledge of OOP, data structures, and algorithms transfers directly. The main adjustment is replacing boilerplate with built-in features.
Is Python slower than Java, C#, or JavaScript?
Usually yes for CPU-bound code, because CPython interprets bytecode. Typical workloads that are I/O-bound or that call native libraries such as NumPy run fast enough. Use multiprocessing, C extensions, or tools like Cython when profiling shows a hotspot.
Does Python have static typing?
Python uses dynamic typing at runtime. Optional type hints (PEP 484) let tools like mypy and pyright catch type errors before execution. The interpreter itself ignores them.
Should I use Python 2 or Python 3?
Use Python 3. Python 2 reached end of life in 2020 and receives no security updates. Target a currently supported 3.x release for new projects.




