Python for Newbies Your Ultimate Guide to Learning Python and Best Practices
Updated: March 27, 2026
TL;DR
Start Python with Python 3.12+, use virtual environments (uv or venv) from day one, follow modern tooling (ruff for linting, pyright for type checking), and write code with type hints for clarity. These practices prevent bad habits and scale smoothly from hobby projects to professional code.
Learning Python is one thing. Learning it right is another. Too many tutorials teach quick-and-dirty habits: no type hints, no linting, no structure. Then learners jump into real projects and struggle with cryptic errors, hard-to-debug code, and deprecated practices they learned years ago. This guide teaches Python fundamentals while embedding best practices from day one — so your code is clean, readable, and professional from your first script.
Step 1: Install Python 3.12+ (Not 2, Not Older 3.x)
Python 3.12 brings better error messages, performance improvements, and optimized bytecode. Match statements (structural pattern matching) were introduced in Python 3.10. Don't use Python 2 (end-of-life 2020) or older Python 3.x versions (outdated). Always choose the latest stable version.
On Windows
- Visit python.org/downloads
- Download Python 3.12 (or latest 3.x)
- CHECK "Add Python to PATH" before installing
- Verify: Open Command Prompt, run
python --version
On macOS
# Using Homebrew (install brew first from brew.sh)
brew install python@3.12
On Linux (Ubuntu/Debian)
sudo apt update
sudo apt install python3.12 python3.12-venv
Step 2: Set Up a Virtual Environment (Critical!)
Never code directly in your system Python. Virtual environments isolate project dependencies so one project's libraries don't break another's. Use uv (faster, simpler) or venv (standard library).
Using uv (Modern, Recommended)
# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# Create a project directory
mkdir my_python_project
cd my_python_project
# Initialize a virtual environment
uv venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
Using venv (Standard, Built-in)
mkdir my_python_project
cd my_python_project
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
Your terminal prompt now shows (.venv) $ indicating the environment is active.
Step 3: Modern Tooling Stack (2026 Standard)
Code Formatter: Ruff
Ruff enforces consistent code style automatically. Install it:
uv pip install ruff
# or with pip: pip install ruff
Create a pyproject.toml file in your project root:
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "W"] # Errors, undefined names, whitespace
Format your code:
ruff format .
Ruff automatically fixes spacing, import order, and style violations.
Type Checker: Pyright
Pyright catches type errors before runtime. Install:
uv pip install pyright
Now write code with type hints:
def calculate_total(price: float, quantity: int) -> float:
"""Calculate order total"""
return price * quantity
result = calculate_total(19.99, 5) # OK
bad = calculate_total("19.99", 5) # Error: string ≠ float
Run pyright:
pyright
It catches the type error instantly without running the code.
IDE: VS Code with Python Extension
Download VS Code for free. Install the Python extension (open VS Code, go to Extensions, search "Python", install Microsoft's).
Configure VS Code for our tooling stack by creating .vscode/settings.json:
{
"python.linting.enabled": true,
"python.linting.ruffEnabled": true,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"python.analysis.typeCheckingMode": "strict"
}
Now VS Code auto-formats code on save and shows type errors in real-time.
Core Python Syntax with Type Hints
Variables and Types
# Type hints make intent clear
name: str = "Alice"
age: int = 30
height: float = 5.9
is_student: bool = False
# Collections with specific types
scores: list[int] = [95, 87, 92]
person: dict[str, int] = {"age": 30, "year": 2026}
Type hints cost nothing at runtime and improve readability massively. They're not optional — they're a best practice.
Functions with Type Hints
def greet(name: str, age: int) -> str:
"""Return a personalized greeting
Args:
name: Person's name
age: Person's age
Returns:
Formatted greeting string
"""
return f"{name} is {age} years old"
greeting = greet("Bob", 25)
Every function should declare input types and return type. This prevents bugs and makes code self-documenting.
Modern Pattern Matching (Python 3.10+)
Python 3.10 introduced match statements (like switch in other languages):
def describe_number(n: int) -> str:
match n:
case 0:
return "Zero"
case 1 | 2: # 1 or 2
return "Small"
case n if n < 0:
return "Negative"
case _: # Default
return "Large"
Much cleaner than chained if/elif/else.
List Comprehensions (Pythonic Way)
# Long way (avoid)
squared = []
for num in range(10):
squared.append(num ** 2)
# Pythonic way
squared = [num ** 2 for num in range(10)]
# With conditions
evens = [x for x in range(20) if x % 2 == 0]
Comprehensions are faster and more readable.
Project Structure: Professional Layout
Organize your project like real code:
my_project/
├── .venv/ # Virtual environment
├── pyproject.toml # Project config (ruff, dependencies)
├── src/
│ └── my_project/
│ ├── __init__.py
│ ├── main.py
│ └── utils.py
├── tests/
│ ├── __init__.py
│ └── test_utils.py
├── .gitignore
└── README.md
This structure scales from hobby project to professional codebase. The __init__.py files (even empty) tell Python these are packages.
Managing Dependencies Properly
Using uv
# Add a dependency
uv pip install requests
# Create a requirements file automatically
uv pip freeze > requirements.txt
Or declare in pyproject.toml:
[project]
dependencies = [
"requests>=2.31.0",
"pandas>=2.0.0"
]
Then install:
uv pip install -e .
Testing: Write Tests from Day One
Testing prevents bugs and makes refactoring safe:
# src/my_project/math_utils.py
def add(a: int, b: int) -> int:
return a + b
# tests/test_math_utils.py
from my_project.math_utils import add
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
assert add(0, 0) == 0
Run tests:
uv pip install pytest
pytest
Small test habits now prevent large disasters later.
Common Beginner Mistakes (And How to Avoid Them)
1. Using Global Variables
# WRONG: Global counter
counter = 0
def increment():
global counter # Ugly and buggy
counter += 1
# RIGHT: Return values
def increment(counter: int) -> int:
return counter + 1
Functions should be independent, not rely on global state.
2. Magic Numbers Without Explanation
# WRONG: What does 0.08 mean?
tax = price * 0.08
# RIGHT: Named constant
TAX_RATE = 0.08
tax = price * TAX_RATE
Constants with descriptive names make intent clear.
3. Ignoring Edge Cases
# WRONG: No error handling
def divide(a: float, b: float) -> float:
return a / b
# RIGHT: Handle division by zero
def divide(a: float, b: float) -> float | None:
if b == 0:
return None
return a / b
Professional code anticipates what can go wrong.
4. Not Using Docstrings
# WRONG: Unclear
def calc(x, y):
return x * y + x / y
# RIGHT: Clear intent
def calculate_avg_rate(price: float, quantity: int) -> float:
"""Calculate average price per unit.
Args:
price: Total price in dollars
quantity: Number of units
Returns:
Average price per unit
"""
return price / quantity
Future you will thank present you for documenting intent.
Practical Project: Todo List App with Best Practices
Let's build a real project using everything learned:
# src/todoapp/main.py
from typing import Optional
from pathlib import Path
import json
class TodoList:
"""Simple todo list manager"""
def __init__(self, file_path: Path = Path("todos.json")):
self.file_path = file_path
self.todos: list[dict[str, str]] = self._load()
def _load(self) -> list[dict[str, str]]:
"""Load todos from file"""
if self.file_path.exists():
with open(self.file_path) as f:
return json.load(f)
return []
def add(self, task: str) -> None:
"""Add a new todo"""
self.todos.append({
"task": task,
"done": False
})
self._save()
def mark_done(self, index: int) -> bool:
"""Mark todo as complete"""
if 0 <= index < len(self.todos):
self.todos[index]["done"] = True
self._save()
return True
return False
def _save(self) -> None:
"""Save todos to file"""
with open(self.file_path, 'w') as f:
json.dump(self.todos, f, indent=2)
def list_all(self) -> None:
"""Display all todos"""
for i, todo in enumerate(self.todos, 1):
status = "✓" if todo["done"] else "○"
print(f"{i}. {status} {todo['task']}")
if __name__ == "__main__":
app = TodoList()
app.add("Learn Python")
app.add("Build a project")
app.list_all()
This code has:
- Type hints throughout
- Docstrings for every method
- Error handling
- File I/O
- Proper naming conventions
- Testable structure
Learning Paths by Goal
Goal: AI/ML Career
- Master Python fundamentals (this guide)
- Learn NumPy, Pandas, Scikit-learn
- Study linear algebra and statistics
- Build 3–5 ML projects for portfolio
Goal: Web Development
- Python fundamentals
- FastAPI or Django framework
- SQL and databases
- Deploy to cloud (AWS, GCP, Heroku)
Goal: Automation/Scripting
- Python fundamentals
- File I/O and regex
- APIs and web scraping (with ethics in mind)
- Task scheduling (cron, APScheduler)
Resources
- Official Python Docs: docs.python.org/3
- Real Python: Comprehensive, well-written tutorials
- Type Hints Guide: mypy.readthedocs.io
- PEP 8 Style Guide: Python's official code style (PEP = Python Enhancement Proposal)
Conclusion
Learning Python properly means starting with best practices, not learning them later. Virtual environments isolate projects, type hints prevent bugs, linting ensures consistency, and proper structure scales from scripts to applications. It takes 10 minutes longer to set up correctly, but saves hours of debugging later. Invest in the right habits now, and your code will remain clean and professional as you grow from beginner to developer.