Getting Started with Python: Overview and Real-World Applications
A practitioner’s guide — not to Python’s feature list, but to the eight concepts that explain most of the bugs, surprises, and ‘why does this behave that way’ moments that developers encounter in their first year with the language.
| WHO THIS IS FOR Beginners confused by Python behaviourDevelopers switching from JavaScript or Java“Why is Python doing this?” — anyone who has thought that | WHAT YOU WILL GAIN A mental model that explains most Python surprises — not just fixes, but understanding. Eight concrete problems, eight root causes, one coherent picture of how the language actually works. |
If you have ever screamed at your screen because Python did something that made no sense — a variable that changed when you never touched it, a function that seemed to remember values from previous calls, an error message pointing at a line that looks completely fine — this is the article that explains why. All of it. And how to stop it happening.
This is not a list of reasons Python is popular. You have read that article. It told you Python is readable, versatile, and has a great community, and you closed it knowing approximately nothing more than when you opened it.
This is the article I wish had existed when I started — the one that explains not just what Python does but why it does it that way, built around the specific moments where the language surprises you. Those surprises are not random. They follow from a small set of design decisions that are entirely coherent once you see them clearly. Eight problems, eight concepts, one consistent model underneath all of them.
Problem 1: Your terminal says Python doesn’t exist
Symptom
You installed Python. You open a terminal. You type python hello.py. You get told Python is not recognised. This happens to almost everyone and has nothing to do with whether you installed Python correctly — it is purely about whether the installer told your operating system where to find it.
Root Cause
On Windows, the Python installer has a checkbox on the first screen: Add Python to PATH. It is not ticked by default. If you missed it, the interpreter is sitting on your machine and the terminal cannot find it. On macOS and Linux, Python 3 is often installed as python3, not python — a naming split left over from the years when Python 2 and 3 coexisted on the same systems.
# What you type:
$ python hello.py
# Windows:
'python' is not recognized as an internal or external command
# macOS / Linux:
python: command not found
Fix
Try python3 –version on macOS/Linux, or py –version on Windows. If either returns a version number, you have Python — it is registered under a different name than the tutorial assumed. If nothing works, reinstall from python.org and tick the PATH checkbox.
Once you have a working interpreter, run the smallest possible program before reading further:
# hello.py
print("Python is working.")
$ python3 hello.py
Python is working.
That output is your baseline. Every error from this point forward is a deviation from a working state, which is a far better debugging position than never having run anything at all.
On editor choice: a plain text editor works until you have a typo in a variable name and no indication where it is. VS Code with the Pylance extension and PyCharm Community both underline errors before you run anything — that feedback loop shortening is worth the ten-minute setup.
Next step: Full installation walkthrough — Windows, macOS, and Linux, including PATH configuration
Problem 2: The error points to a line that looks fine
Symptom
You get a SyntaxError on a line you are certain is correct, or an error message that references line forty-three when the actual mistake is on line twelve. You re-read line forty-three. Nothing wrong. This is one of those moments where understanding the pipeline turns a five-minute confusion into a five-second diagnosis.
Root Cause
Python does not read your code and execute it line by line. It processes your file in four stages before any output appears. The lexer converts source text into tokens. The parser takes those tokens and checks that they form a grammatically valid program, producing an Abstract Syntax Tree. The compiler converts that tree into bytecode. Then — and only then — the Python Virtual Machine begins executing.

A SyntaxError means the parser failed. The grammar check runs across your entire file before a single line executes — which is why you can have a SyntaxError in a function that would never have been called. The error message points to where the parser noticed the problem, which is often not where the problem actually is. A missing opening bracket on line five typically surfaces as a SyntaxError on whichever line the parser reaches when it realises the bracket was never closed.
name = 'Alice'
score = 95
print name, score) # missing opening bracket
# Python reports:
File 'example.py', line 3
print name, score)
^^^^^
SyntaxError: invalid syntax
# The parser flagged 'name' because it followed 'print' without a bracket.
# Your fix is on line 3, but now you know *why* it flagged that location.
Runtime errors — NameError, TypeError, AttributeError, IndexError — are different. The parser and compiler succeeded. The PVM reached an instruction it could not complete. These point to the actual line of execution, which is usually where the problem is.
Fix
For SyntaxErrors: look above the reported line, not at it. Search for an unclosed bracket, quote, or parenthesis somewhere earlier. For runtime errors: the line number is accurate — start there.
The .pyc files in __pycache__ are cached bytecode. Python checks the source modification timestamp; if it has not changed, it skips the first three stages. Deleting __pycache__ is always safe — Python rebuilds it silently on the next run.
Problem 3: Changing one variable changes another one
Symptom
You assign a list to a second variable so you can keep the original while working with a modified copy. Both change. No error. The logic looks sound. Reading the code again does not help because the bug is not in the code — it is in the mental model of what assignment means.
prices = [100, 200, 300]
discounted = prices
for i in range(len(discounted)):
discounted[i] = round(discounted[i] * 0.9, 2)
print('original: ', prices)
print('discounted:', discounted)
original: [90.0, 180.0, 270.0] <-- both changed. Not what you wanted.
discounted: [90.0, 180.0, 270.0]
Root Cause
In Python, a variable is not a box containing a value. It is a name bound to an object in memory. When you write discounted = prices, Python creates a second name pointing to the exact same list. There is still only one list. Both names are labels on it. Modifying discounted modifies the list that both names reference.
prices = [100, 200, 300]
discounted = prices
print(id(prices) == id(discounted)) # True -- same object in memory
Fix
Ask for a copy explicitly:
discounted = prices.copy() # list method -- works for flat lists
discounted = list(prices) # reconstruct from iterable
discounted = prices[:] # full slice
# For a list containing other mutable objects (nested lists, dicts):
import copy
discounted = copy.deepcopy(prices)

The same principle explains why passing a list into a function and modifying it inside also changes the original. The function receives the same object, not a copy. This is not a bug — it is the expected behaviour of name binding. But it only becomes predictable once you have the model.
Problem 4: Your function modified the data it was only supposed to read
Symptom
A function returns the right values but also permanently overwrites its input. You do not discover this until several steps later when you need the original data and find it has already been transformed.
def apply_discount(cart, rate):
for i in range(len(cart)):
cart[i] = round(cart[i] * (1 - rate), 2)
return cart
order = [49.99, 19.99, 9.99]
checkout = apply_discount(order, 0.10)
print(order) # [44.99, 17.99, 8.99] -- the original is gone
Root Cause
The function receives a reference to the same list order points to. Modifying cart inside the function modifies the shared object. When the function returns, order still points to it — now modified.
Fix
Build and return a new object instead of modifying the input. List comprehensions make this natural:
def apply_discount(cart, rate):
return [round(price * (1 - rate), 2) for price in cart]
order = [49.99, 19.99, 9.99]
checkout = apply_discount(order, 0.10)
print('order: ', order) # [49.99, 19.99, 9.99] -- intact
print('checkout:', checkout) # [44.99, 17.99, 8.99] -- correct
In-place mutation has legitimate uses — sorting a large dataset, updating a shared cache. The rule is not never mutate, it is be explicit about it. If a function modifies its input, say so in the name, say so in the docstring, and make it a deliberate decision rather than an accidental side effect.
Problem 5: Your function remembers old values between calls
Symptom
You call a function multiple times and the results accumulate across calls — even though you never stored anything between them. Each call appears to have inherited state from the previous one.
def add_item(cart=[]):
cart.append('new item')
return cart
print(add_item()) # ['new item']
print(add_item()) # ['new item', 'new item'] <-- where did this come from?
print(add_item()) # ['new item', 'new item', 'new item']
This is one of the most famous Python surprises. It appears in Python’s own documentation under common gotchas, and developers who have been writing Python for years still walk into it.
Root Cause
Default argument values are evaluated once, when the function is defined — not each time the function is called. That [] in def add_item(cart=[]) is a single list object, created at the moment Python processes the def statement. Every call to add_item() that uses the default shares that same list. Appending to it makes the change visible on the next call, and the call after that, indefinitely.
![Side by side comparison showing Python mutable default argument bug where cart=[] accumulates items across three function calls versus the correct cart=None sentinel pattern that creates a fresh list each call.](https://emitechlogic.com/wp-content/uploads/2026/03/viz3-mutabledefault-1002x1024.png)
You can see the object persisting between calls:
def add_item(cart=[]):
cart.append('new item')
return cart
first = add_item()
second = add_item()
print(first is second) # True -- same list object returned both times
Fix
Use None as the default and create a fresh object inside the function. This is the canonical Python pattern for mutable defaults:
def add_item(cart=None):
if cart is None:
cart = []
cart.append('new item')
return cart
print(add_item()) # ['new item']
print(add_item()) # ['new item'] <-- independent each time
print(add_item()) # ['new item']
The None default is evaluated once at definition time, same as before — but None is immutable and you never modify it. The fresh [] is created inside the function body each time the function is called without an argument, so every call that needs a new list gets one.
This pattern cements a broader rule: defaults are evaluated at def time, not at call time. It applies to any mutable default — lists, dicts, sets, class instances. If the default should be a fresh object per call, use None as the sentinel.
Problem 6: A number that isn’t a number
Symptom
You read a value from user input, a CSV file, or a web response. You try to do arithmetic with it. Python raises a TypeError and refuses. The value looks like a number. It is not one.
from datetime import date
birth_year = input('What year were you born? ') # user types 1992
current = date.today().year
age = current - birth_year
TypeError: unsupported operand type(s) for -: 'int' and 'str'
Root Cause
input() always returns a string. Every time, without exception. “1992” and 1992 are different objects — one is a sequence of four characters, one is an integer. 2025 – “1992” is meaningless to Python. The refusal is not unhelpfulness; it is Python preferring a loud failure over a silent wrong answer. If the user had typed “nineteen ninety-two”, a silent conversion would have produced garbage.

Fix
Convert at the boundary — immediately when data enters your program, before it travels further:
from datetime import date
raw = input('What year were you born? ')
try:
birth_year = int(raw)
except ValueError:
print(f'That does not look like a year: {raw!r}')
raise SystemExit(1)
age = date.today().year - birth_year
print(f'You are approximately {age} years old.')
The same discipline applies whenever data arrives from a file, a network response, a database query, or a CSV column. It arrives as text. Convert once, at the entry point, with explicit error handling. Do not let raw strings travel through your program and convert them deep inside a function where a ValueError five layers down is much harder to trace.
Non-obvious conversions worth knowing: int(3.9) truncates to 3, not 4. bool(“”), bool(0), bool([]), and bool(None) are all False. str(None) is the four-character string ‘None’, not an empty string.
Problem 7: The code is correct but nobody knows what it does
Symptom
No error. The code runs. But six months later — or six days later — you open the file and cannot tell what it does without tracing every line.
def proc(d, x, fl=True):
r = []
for i in d:
if fl:
r.append(round(i * x, 2))
else:
r.append(i)
return r
What does d contain? What is x? What does fl control? What does proc stand for? You cannot answer any of these without tracing the logic — and in a real codebase, this function gets called from three other places, each with differently-named arguments, making the trace longer every time.
Root Cause
No naming discipline and no documentation. Python enforces neither — it is entirely possible to write correct Python with single-letter variable names and zero docstrings. The interpreter does not care. Your colleagues do. Your future self does.
Fix
Python’s answer is PEP 8 naming conventions combined with type hints and docstrings. The conventions are: functions and variables use snake_case; classes use PascalCase; module-level constants use UPPER_SNAKE_CASE; a leading underscore signals private by convention; dunder methods like __init__ and __len__ participate in Python’s data model.
TAX_RATE = 0.20
def calculate_totals(
prices: list[float],
rate: float,
apply_tax: bool = True
) -> list[float]:
"""
Apply an optional multiplier to each price in the list.
Args:
prices: List of unit prices in USD.
rate: Multiplier to apply, e.g. 1.20 for 20% markup.
apply_tax: If False, return prices unchanged.
Returns:
New list of processed prices, rounded to 2 decimal places.
"""
if not apply_tax:
return list(prices)
return [round(p * rate, 2) for p in prices]
One distinction worth making explicit: a comment (#) is for the reader of the source code and is stripped by the compiler. A docstring is part of the compiled program — accessible at runtime via .__doc__, read by IDEs for auto-complete hints, and parsed by documentation tools. A docstring is not a decorated comment. It is executable documentation with a different job.
Problem 8: The file path is correct but the file cannot be found
Symptom
You write a Windows file path as a string. You print it and find newlines in it. Or you write a regular expression that matches nothing, or matches the wrong things, and the pattern looks completely correct when you re-read it.
# A Windows developer writes a log file path:
log_path = "C:\Users\nadia\projects\notes.txt"
print(log_path)
C:\Users
adia\projects
otes.txt
# \n became a newline. Three times.
# The path has newline characters in it.
# No error was raised. Python processed it silently at parse time.
Root Cause
Inside a double-quoted string, a backslash is the start of an escape sequence. \n is newline. \t is tab. \r is carriage return. Python processes these before the string reaches any function. The sequence \n in your source becomes a single newline character in memory — silently, without warning, at parse time.
Fix
A raw string — prefixed with r — turns off escape sequence processing entirely. Every backslash is treated as a literal backslash. The r prefix changes how the source literal is parsed; the result is still an ordinary str object.
# Raw string prefix
log_path = r"C:\Users\nadia\projects\notes.txt"
print(log_path) # C:\Users\nadia\projects\notes.txt -- correct
# Forward slashes also work on Windows
log_path = "C:/Users/nadia/projects/notes.txt"
# pathlib -- modern cross-platform approach
from pathlib import Path
log_path = Path("C:/Users/nadia/projects") / "notes.txt"
Regular expressions have the same problem. The re module uses \d for digits, \w for word characters, \s for whitespace. Without a raw string, Python interprets these before re sees them, which silently corrupts the pattern:
import re
# Wrong -- Python consumes \d before re sees it
match = re.search("\d+", "Order 42") # unreliable
# Correct -- r prefix passes \d intact to re
match = re.search(r"\d+", "Order 42") # matches '42'
Rule of thumb: any string containing backslashes intended as literal characters should be a raw string. Windows paths, regular expressions, LaTeX markup, Windows registry keys.
What these concepts enable in practice
The eight problems above are not beginner problems that disappear as you advance. They recur throughout a Python career in slightly different forms — in data science scripts, web services, automation pipelines, and production systems. Seeing them in context clarifies why Python’s design made the choices it did.
Data science and machine learning
Python did not win in data science because of syntax. It won because the language is extensible at the C level — computationally heavy operations can be implemented in compiled code and called from Python as if they were regular functions. NumPy’s array operations run in compiled C. Pandas builds on NumPy. TensorFlow and PyTorch delegate neural network computation to CUDA kernels, with Python as the interface layer. You write readable Python; the computation runs at hardware speed.
The name-binding and mutability rules matter immediately in this domain. When you slice a Pandas DataFrame — subset = df[df[‘score’] > 80] — you may get a view into the original DataFrame or an independent copy, depending on how the slice was created. Modifying a view modifies the original. Pandas issues a SettingWithCopyWarning when it detects the ambiguity. Understanding how Python’s mutable objects and name binding actually work is what makes that warning legible rather than mysterious.
Web development
Django’s design philosophy — one correct way to do things — is PEP 20 applied to a web framework. It provides an ORM, admin interface, authentication, URL routing, and templating in a single coherent package. Instagram, Pinterest, and Disqus have run on it at scale. FastAPI takes a more recent approach: it uses Python’s type annotation syntax to automatically validate request parameters and generate API documentation. The annotations that improve readability also become runtime enforcement — one piece of code, multiple jobs.
Automation
Python’s standard library covers most automation tasks without third-party packages. pathlib for the file system. subprocess for processes. smtplib for email. csv, json, sqlite3 for data. The type conversion discipline from Problem 6 matters at every boundary — files, HTTP responses, database results all arrive as text or bytes. Convert at the entry point, handle failures explicitly, and your scripts will be reliable in production rather than fragile in staging.
A note on Python implementations
When you install from python.org, you get CPython — the reference implementation, used by almost everyone. Two situations make the alternatives worth knowing. If you need deep Java or .NET integration, Jython and IronPython run on the JVM and CLR respectively. The tradeoff: C extensions like NumPy and Pandas do not work in either. If you have a CPU-bound Python program that profiling confirms is bottlenecked on Python code rather than library calls, PyPy runs two to ten times faster for that workload without changing your code. The CPython vs Jython vs IronPython guide covers the decision criteria for each.
One final thought
The eight problems in this article are not beginner problems that disappear as you advance. Experienced developers walk into the mutable default trap. Teams with years of Python experience write functions that silently modify their inputs. The difference between a junior developer and a senior one is not that the senior stops encountering these problems — it is that they recognise them in under ten seconds.
That recognition comes from the mental model, not from memorising fixes. The model is: variables are labels, not boxes. Defaults are evaluated once at definition time. Objects are either mutable or they are not. Types do not convert silently at boundaries. The parser runs before execution and rejects the entire file. Naming is communication, not style.
Install Python if you have not already. Run the code blocks in this article. Break them deliberately — pass a mutable default, assign a list to two names, pass a string into arithmetic. Watch what happens. Then read the explanation again. It will mean something different the second time.
The understanding you get from a failed experiment is worth more than the understanding you get from reading a correct explanation. Python is patient. Run things.
Python Fundamentals — Continue Reading
Get Python installed on Windows, macOS, or Linux. Configure PATH so your terminal can find the interpreter. Write hello.py and run it. This is your baseline — everything else assumes this works.
Configure either PyCharm Community or VS Code with the Python extension for real development. Covers virtual environments, linting with Pylance/Flake8, and IntelliSense that surfaces errors before you run anything.
The full four-stage pipeline from source file to PVM execution. Why SyntaxErrors appear before any code runs. What __pycache__ stores. How to inspect bytecode with the dis module and what those instructions mean.
How Python binds names to objects rather than storing values in boxes. The LEGB scope lookup rule. Why ALL_CAPS constants are a convention the interpreter does not enforce, and how typing.Final adds tooling enforcement.
PEP 8 naming for every Python construct: snake_case for functions and variables, PascalCase for classes, UPPER_SNAKE_CASE for constants, single/double underscore prefixes, dunder methods. The reasoning behind each rule, not just the rule itself.
Every object in Python is mutable or immutable — this determines whether assignment copies or shares, whether function arguments can be modified, and why the mutable default argument trap exists. Covers copy vs deepcopy in detail.
When Python converts types automatically and when you must do it manually. Edge cases that bite: int(3.9) truncates not rounds, str(None) is ‘None’ not empty string, bool([]) is False. The convert-at-the-boundary habit that prevents most type bugs.
Sequences (list, tuple, range), mappings (dict), and sets (set, frozenset) in depth. How each type is stored in memory, which operations each supports, and the performance characteristics that matter in practice.
All sixteen escape characters. Why Windows file paths break silently without the r prefix. Why regular expressions need raw strings. Unicode escapes, byte strings, and the exact patterns that fail without you knowing.
Comments should explain why, not what — the code already shows what. What commented-out code signals about a codebase. The difference between # comments (stripped by compiler) and docstrings (executable documentation that tools read).
CPython is what almost everyone runs. Jython integrates with the JVM. IronPython with .NET. PyPy uses JIT compilation for 2–10x performance on CPU-bound code. When the default implementation is not the right choice — and what you give up by switching.
The three major docstring formats — Google style, NumPy style, and reStructuredText — compared side by side. When each is appropriate. Writing documentation that IDE tools, Sphinx, and future team members can all use.
Setting up Sphinx and autodoc. Deploying to Read the Docs. Integrating Python type hints into your documentation pipeline so annotations in code become API reference automatically.
Frequently Asked Questions
Is Python good for someone who has never programmed before?
Yes, and there is a specific reason. Python’s indentation rule — the thing that looks like a quirk — forces you to structure code into blocks in a way that maps directly to how logic works: this condition, then this group of instructions, otherwise this other group. Other languages let you write code that is structurally misleading. Python does not. For a beginner building a mental model of what programs actually do, that constraint is a feature, not a restriction.
The practical evidence: MIT, Stanford, and Carnegie Mellon have all moved introductory CS courses to Python. Not because it is a toy language — those programs teach students who go on to write compilers and operating systems — but because Python removes syntactic complexity that causes beginners to conflate language mechanics with programming logic. That conflation is the most common reason people quit early.Python 2 or Python 3?
Python 3, without discussion. Python 2 reached official end-of-life in January 2020 — no security updates, no bug fixes, no new features. If you find a tutorial that teaches Python 2, it is outdated. If you inherit a Python 2 codebase at work, migrating it is the right response, not continuing to write Python 2.
I keep hearing Python is slow. Should I use something else?
The framing misleads. CPython is slower than compiled languages for CPU-bound work — that is genuinely true. But most Python programs spend their time waiting: for network responses, disk reads, database queries. For that category of workload, Python’s performance is entirely adequate.
For heavy computation, the scientific stack delegates to compiled code. NumPy, PyTorch, TensorFlow — the Python layer orchestrates, the compiled layer computes. A Python for loop over ten million numbers is slow because it executes Python bytecode per iteration. A NumPy vectorised operation over the same data executes compiled C once across the array. That is why TensorFlow training is fast despite the interface being Python.
Where Python genuinely is too slow: tight pure-Python loops over very large data, real-time systems with hard latency requirements. For those cases, PyPy often helps without code changes. Rewriting everything in another language is rarely the correct first response.Should I start with .py scripts or Jupyter Notebooks?
For data exploration and analysis: start with Jupyter. The cell-by-cell execution model lets you inspect data at each step without re-running the whole file, which is exactly what exploratory work needs. For everything else — web applications, automation, CLI tools, anything that runs unattended — write .py files. Notebooks are awkward to version control, test, and deploy. Excellent for exploration and explanation; not production code.
How long does it take to learn Python?
The syntax takes days of focused effort. Writing small scripts that actually do something useful takes a couple of weeks. The eight concepts in this article — internalised so bugs feel familiar rather than mysterious — take months of deliberate practice and breaking things on purpose. Writing production-quality Python that other developers can maintain and extend without asking you questions: a year or two of working in a real codebase with genuine feedback.
The most common mistake is treating ‘I know the syntax’ as ‘I know Python’. Knowing the syntax is like knowing the alphabet — necessary but not sufficient. The meaningful threshold is writing code that your future self can understand in three months without retracing every decision.Do I need to understand all of this before I start writing code?
No — and the reverse is more efficient. Start writing code. Hit the specific problems in this article. Then return to the relevant section. Understanding is faster when you already have the error message loaded in working memory. The explanation lands differently when you have just spent twenty minutes confused about the exact thing it is explaining.
What is the difference between a comment and a docstring?
A comment (#) is for the reader of the source file. It is stripped by the compiler and never makes it into the bytecode. A docstring is part of the compiled program — stored as the .__doc__ attribute of the function, class, or module, accessible at runtime, read by IDE auto-complete systems, and parsed by documentation generation tools like Sphinx. help(your_function) in the REPL displays the docstring. They are not the same thing at different verbosity levels — they have different audiences and different lifetimes.
The documentation guide, Part 1 covers the three major docstring formats — Google, NumPy, and reStructuredText — and when each is the appropriate choice.I’m switching from JavaScript. What bites first?
The name-binding and mutability model from Problems 3 and 4. In JavaScript, assignment creates a copy for primitives and a reference for objects. In Python, assignment always creates a binding — you always get reference behaviour, for everything. The types that are immutable (integers, strings, tuples) behave like copies in practice, but the mechanism is different, and it matters the moment you start working with lists and dictionaries.
The mutable default argument trap from Problem 5 also catches JavaScript developers early, because there is no direct equivalent in JS — a function’s default parameter expression is re-evaluated on every call that uses it. In Python, it is evaluated once at definition time.
Third: Python uses is for identity comparison and == for equality. Always use x is None, never x == None — the latter can be overridden by a class’s __eq__ method and produce unexpected results. For True and False, the same rule applies: x is True rather than x == True.
Further Reading (Recommended Resources)
- Python Official Documentation – Complete language reference
https://docs.python.org/3/ - Python Gotchas Guide
https://docs.python-guide.org/writing/gotchas/ - NumPy Documentation
https://numpy.org/doc/ - FastAPI Documentation
https://fastapi.tiangolo.com/

Leave a Reply