dead simple python
your workbench
Packages and Virtual Environments
A package is a collection of code, which is analogous to a library in most
other programming languages.
Some packages require other packages to be installed first. Certain packages
have conflicts with other packages. You can also install specific versions
of a package, depending on what exactly you need.
This is why virtual environments exist.
A virtual environment is a sandbox where you can install only the
Python packages you need for a particular project, without the risk of
those packages clashing with those for another project (or your system).
You never
actually change what Python packages are installed on your system, so
you avoid breaking important things that have nothing to do with your
project.
You may even create virtual environments that have nothing to do with
a particular project.
Creating a Virtual Environment
To create the virtual environment with the name venv_1 in the current
working directory, run the following command in the command line:
python3 -m venv venv_1
venv_1 is the desired path to the virtual environment. In this
case, venv is just a relative path, creating a venv/ directory in the current
working directory. However, you could also use an absolute path, and you
could call it whatever you want
python3 -m venv /opt/myvirtualenv
vscode
python create environment
Activating a Virtual Environment
On Windows, run this:
venv_1\Scripts\activate.bat
venv_1/Scripts/activate.bat
vscode
python select interpreter
If you have multiple shells (usually terminal windows) open, you should be
aware that the virtual environment is only active for the one shell you explicitly
activated it in!
environment paths will be overridden by the virtual environment.
Practically speaking, any packages you install in the virtual environment
are only usable there, and from the venv, the system-wide packages cannot
be accessed unless you explicitly specify otherwise.
If you want the virtual environment to also see the system-wide packages, you can do so with a special flag
python3 -m venv –system-site-packages venv_1
Leaving a Virtual Environment
venv_1\Scripts\deactivate.bat
Introducing pip
System-Wide Packages
python3 -m pip command
Installing Packages
pip install package
If you want to install a specific version of something, append two equal
signs (==), followed by the desired version number (no spaces):
pip install PySide6==6.1.2
Bonus: you can even use operators like >=, to mean “at least this version
or greater.” These are called requirement specifiers.
pip install PySide6>=6.1.2
If you’re on a UNIX-like system, you may need to use pip install
“PySide6>=6.1.2” instead, since > has another meaning in the shell.
requirements.txt
writing a requirements
.txt file for your project. This file lists the packages your project needs. When
creating a virtual environment, you and other users can install all the
required packages with a single command, using this file.
To create this file, list the name of a pip package, as well as its version (if
required), on each line.
requirements.txt
1 | PySide2>=5.11.1 |
install all those packages in one shot with this
command:
pip install -r requirements.txt
Upgrading Packages
pip install –upgrade PySide6
pip install –upgrade -r requirements.txt
Removing Packages
pip uninstall package
Installing one package will also install any
other packages it relies upon, which we call its dependencies. Uninstalling a
package does not remove its dependencies, so you may need to go through
and remove them yourself.
Herein lies one more advantage of virtual environments. Once I’m in
that sort of a pickle, I can delete the virtual environment, create a new one,
and install only those packages I need.
Seriously. Never use sudo pip
Virtual Environments and Git
If you’re using Git, create or edit a file called .gitignore in the root directory of your repository. Add this line somewhere in it:
venv_1/
the whole shebang
The shebang (short for haSH-BANG, or #!) 1 provides the path to the
Python interpreter. While it’s optional, I strongly recommend including
it in your code, as it means the file can be marked as executable and run
directly, like this:
./hello_world.py
hello_world.py
1 | #!/usr/bin/env python3 |
If you happen to have a script which runs in both Python 2 and Python 3,
use this shebang instead:
#!/usr/bin/env python
File Encoding
Since Python 3.1, all Python files have used the UTF-8 encoding, allowing the
interpreter to use all of the characters in Unicode. (Prior to that version,
the default was to use the old ASCII encoding.)
# -*- coding: latin-1 -*-
# coding: latin-1
# This Python file uses the following encoding: latin-1
Whatever you use, it must be exactly as shown above, except for swapping latin-1 for whatever encoding you want. For this reason, the first or
second forms are preferred.
A Few Extra Virtual Environment Tricks
Using a Virtual Environment Without Activating
assuming my virtual environment is venv, I could do this
in the terminal:
1 | venv/bin/pip install pylint |
import pylint still will not work on the system-wide Python
interactive shell
Quality Control: Static Analyzers
static
analyzer, which reads your source code, looking for potential problems or
deviations from the standards.
One common type of static analyzer, called a linter,
checks your source code for common mistakes, potential errors, and style
inconsistencies. Two of the most popular linters are Pylint and PyFlakes.
Pylint
Style Janitors: Autoformatting Tools
an autoformatter, which can automatically change your Python code—spacing, indentation, and preferred equivalent expressions (such as != instead of <>)—to be PEP 8 compliant. Two
options are autopep8 and Black.
An Exhibition of Code Editors
PyCharm
The
professional edition adds tools for data, scientific development, and web
development.
SYNTA X CR ASH COURSE
Statements and Expression
Each line of code in Python that ends with a line break is a statement,
sometimes more specifically known as a simple statement.
A section of code that evaluates to a single value is called an expression.
put expressions nearly anywhere a value is expected.
The expression is evaluated down to a value, and that value is used in that
position in the statement.
The Importance of Whitespace
PEP 8 style guide stresses using either four spaces or a single tab per indentation level. Consistency is key! Python really doesn’t care whether you use
tabs, two spaces, four spaces, or even seven spaces (although that’s probably
a step too far) for each level of indentation. The point is to be consistent
within any and every given block of code.
Doing Nothing
On occasion, you will need to insert a statement that has absolutely no
effect. This is particularly useful when you need to put a syntactically valid
placeholder where a suite of code will exist later. For this purpose, Python
provides the pass keyword.
1 | raining = True |
Comments and Docstrings
To write comments in Python, precede the line with a hash (#). Everything
between the hash and the end of the line is a comment and will be ignored
by the interpreter.
Docstrings
Docstrings exist to provide documentation for functions, classes, and
modules, especially public ones. They conventionally begin and end with
three quotation marks (“””), allowing the string to automatically span multiple lines. You would typically place docstrings at the top, inside of whatever they’re defining, such as in the function above.
There are three important distinctions between comments and
docstrings:
- Docstrings are string literals, and they are seen by the interpreter; comments are ignored.
- Docstrings are used in automatic documentation generation.
- Docstrings are generally only docstrings when they appear at the top of
the module, function, class, or method they define. Comments can live
anywhere.
It is perfectly possible to use a triple-quoted string literal to write a sort
of “multiline comment,” but it’s not recommended, since a string literal can
easily get left in a place where Python will try to use it as a value.
Declaring Variables
Python is dynamically typed, meaning the data type of a value is determined when it is evaluated. This contrasts with statically typed languages,
in which you declare the data type initially. (C++ and Java are both statically
typed.)
using the
assignment operator (=). It infers the data type. If the name is a new variable, Python will create it; if the name already exists, Python will change
the value. It’s a pretty straightforward system.
two rules to follow with Python variables:
- Define a variable before you access it; otherwise, you’ll get an error.
- Don’t change what kind of data you’re storing in the variable, even
when replacing a value.
Python is considered a strongly typed language, meaning you usually can’t
magically combine data of different types. For example, it won’t allow you
to add an integer and a string together.
Python is, however, weakly bound, so it is possible to assign a value of a
different type to an existing variable. While this is technically permissible,
it is strongly discouraged, as it can produce confusing code.
What About Constants?
Python doesn’t have any formally defined constants. In keeping with PEP 8,
you would indicate a variable is intended to be treated as a constant by
using all-caps names with underscores. This naming convention is sometimes humorously referred to as screaming snake case for the all-caps (screaming) and the underscores (snakes).
For example, the name INTEREST_RATE
Mathematics
Meet the Number Types
Integers (int) store whole numbers. In Python, integers are always signed
and effectively have no maximum value. Integers use decimal base (base-10)
by default,
Floating-point numbers (float) store numbers with a decimal part (for
example, 3.141592).
Internally, values are stored as double-precision, IEEE 754 floatingpoint numbers, which are subject to the limits inherent in that format.
specify an invalid number with float(“nan”), a number
larger than the largest possible value with float(“inf”), or a number smaller
than the smallest possible value with float(“-inf”).
Decimal stores fixed-point decimal numbers, while Fraction
does the same for fractions
To use either, you’ll need to import them first.
1 | from decimal import Decimal |
1 | print(-42) # negative (unary), evaluates to -42 |
Python offers augmented
assignment operators, sometimes informally called compound assignment
operators. These allow you to perform an operation with the current value
of the variable as the left operand.
1 | foo = 10 |
the divmod() function to efficiently perform the calculation, returning the two results in a tuple. Thus, c = divmod(a, b) is the
same as c = (a // b, a % b).
bitwise operators
1 | print(9 & 8) # bitwise AND, evaluates to 8 |
the math module
1 | import math |
1 | import math |
Comparison Operators
1 | score = 98 |
Boolean, None, and Identity Operators
Take particular note of the last condition, eggs is spam which illustrates an important gotcha with the is operator. It actually compares the identity of the
variables, rather than the value
1 | spam = True |
Truthiness
Most expressions and values in Python can be evaluated to a True or False
value. This is typically done by using the value as an expression by itself,
although you can also pass it to the bool() function to convert it explicitly.
1 | answer = 42 |
When an expression will evaluate to True, it is considered “truthy.”
When it will evaluate to False, it is “falsey.”
The None constant, values representing zero, and empty collections are all considered “falsey,” while most
other values are “truthy.”
Logical Operators
1 | spam = True |
1 | score = 98 |
The condition employing the != operator is preferred for readability.
The Walrus Operator
Python 3.8 introduced assignment expressions, which allow you to assign a
value to a variable and use that variable in another expression at the same
time. This is possible with the so-called walrus operator (:=).
1 | if (eggs := 7 + 5) == 12: |
Python first evaluates the expression on the
left (7+5) and then assigns it to the variable eggs.
The assignment expression is then evaluated to a single value, namely
the value of eggs, which is used in the comparison
Two particularly useful style rules are put forth
by this PEP:
• If either assignment statements or assignment expressions can be used,
then prefer statements; they are clear declarations of intent.
• If using assignment expressions would lead to ambiguity about execution order, then restructure to use statements instead.
Strings
String Literals
The only time you’d really need to escape either single or double
quotes with backslashes would be if you had both types of quotes in the
string at once:question = "What do you mean, \"it's fine\"?"
triple quotes define multiline string literals. In other
words, I can use them to do this:
1 | parrot = """\ |
The only exception occurs when you use a backslash (\)
to escape
particular character, like I did with that newline at the beginning 1. It is
conventional to escape the first newline after the opening triple quotes, just
to make the code look cleaner.
concatenate (combine) string literals, simply by
writing them next to one another, without any operators between them. For
example, spam = “Hello “ “world” “!” is valid, resulting in the string Hello
world!.
Raw Strings
Raw strings constitute another form of string literal, wherein the backslash
(\
) is always treated as a literal character. They’re preceded with an r, such
as in this example:print(r"I love backslashes: \ Aren't they cool?")
Formatted Strings
1 | in_stock = 0 |
Instead, you’d have to
evaluate that expression in advance, assign the result to a name, and use it
in the f-string.
The ord() function returns the number representing the unicode code of a specified character.
print(f”{ord(‘\n’)}”) # SyntaxError
newline_ord = ord(‘\n’)
print(f”{newline_ord}”) # prints “10”
Format Specifications
Immediately after the expression, you may choose to include one of
three special flags: !r, !a, or !s (although that last one is the default behavior, so it can be omitted in most cases). These determine which function
is used to fetch the string representation of some value: repr(), ascii(), or
str(), respectively
Next comes the format specification itself, which always begins with a
colon (:), followed by one or more flags. These have to be specified in a particular order to work, although any of them may be omitted if they’re not
desired:
Align An alignment flag, specifying left (<), right (>), center (^), or (if
numeric) split with the sign aligned left but the digits aligned right (=).
This is optionally preceded by a character that will be used to fill any
blank space in the alignment.
Sign A flag controlling when the sign is displayed on a number. The
plus (+) flag displays the sign on both positive and negative numbers,
while the minus (–) flag only displays it on negative numbers. A third
option is to show a leading space on positive numbers and a sign on
negative numbers (SPACE).
Alternative form The hash (#) flag turns on the “alternative form,”
which has different meanings for different types (see documentation).
Leading zeros The zero (0) flag causes leading zeros to be displayed
(unless a fill character is specified for alignment).
Width The width of the output string in characters. This is where the
alignment comes into play.
Grouping A flag controlling whether numbers should separate thousands with a comma (,) or an underscore (_). If omitted, no separator
is used. If enabled, the underscore separator also appears every four
digits in octal, hexadecimal, and binary numbers.
Precision A dot (.), followed by an integer for decimal precision
Type A flag controlling how numbers are displayed; common options
include binary (b), character (c), decimal (d), hexadecimal (x), exponent notation (e), fixed-point (f), and general (g). There are more (see
documentation).
1 | spam = 1234.56789 |
String Conversion
str(), is the one you’ll use most often, as it returns
the human-readable representation of the value.
repr() returns the canonical string representation of the value:
that is, (usually) the value as Python sees it. In the case of many basic data
types, this will return the same thing as str(), but when used on most
objects, the output contains additional information useful in debugging.
The ascii() function is the same as repr(), except that the string literal
it returns is completely ASCII-compatible, having escaped any non-ASCII
(for example, Unicode) characters.
A Note on String Concatenation
Typical concatenation with + or the join() function has the same result,
but the latter function will be as fast or faster, especially when you’re using
other implementations of Python besides CPython. Therefore, whenever
you need to concatenate and f-strings aren’t right for the job, you should
consider using join() instead of the + or += operators. In practice, f-strings
are the fastest, but join() is your next-best option.
1 | greeting = "Hello" |
Functions 0
Python functions are first-class citizens, which means they can be treated like
any other object
Classes and Objects 0
Error Handling 0
1 | num_from_user = input("Enter a number: ") |
Tuples and Lists
Lists constitute the most array-like collection in Python. In CPython,
they are implemented as variable-length arrays
A tuple is somewhat similar to a list, but with a few key differences.
First, a tuple cannot have items added, reassigned, or removed after its creation. Attempting to modify the contents of a tuple with bracket notation
will result in a TypeError. This is because tuples, unlike lists, are immutable, effectively meaning their contents cannot be modified
The guideline is to use tuples for collections of items of different types
(heterogeneous collections) and to use lists for collections of items of the same
type (homogeneous collections).
Loops
Structural Pattern Matching
Python 3.10 gained structural pattern matching via PEP 634.
This provides conditional logic that is at least syntactically similar to switch
statements of other languages. In short, you test a single subject, such as a
variable, against one or more patterns. If the subject matches the pattern,
the associated suite of code runs.
lunch_order = input(“What would you like for lunch? “)
match lunch_order:
case ‘pizza’:
print(“Pizza time!”)
case ‘sandwich’:
print(“Here’s your sandwich”)
case ‘taco’:
print(‘Taco, taco, TACO, tacotacotaco!’)
case _:
print(“Yummy.”)
The underscore (_) in the last case is the wildcard, which will match any
value. This serves as a fallback case, and it must come last, as it will match
anything.
PROJECT STRUCTURE AND IMPORTS
Setting Up the Repository
Your Python code belongs in a separate subdirectory and not in the
root of the repository. This is very important, as your repository’s root directory will get mighty cluttered with build files, packaging scripts, documentation, virtual environments, and all manner of other things that aren’t
actually part of the source code.
Modules and Packages
A module is any Python (.py) file.
A package, occasionally called a regular package, is one or more modules within a directory. That directory must include a file called __init__
.py (which can be empty). The __init__
.py file is important! If it isn’t there,
Python will not know the directory constitutes a package.
PEP 8 and Naming
Modules should have short, all-lowercase names. Underscores can
be used in the module name if it improves readability. Python
packages should also have short, all-lowercase names, although
the use of underscores is discouraged.
Do this: omission/data/data_loader.py
NOT this: omission/Data/DataLoader.py
another special file in my top-level package: __main__.py
. This
is the file that runs when I execute my top-level package directly, via this
command:
Import Dos and Don’ts
1 | #!/usr/bin/env python3 |
The namespace of open() and close() is smart_door. A namespace is an
explicitly defined path to something, such as a function. The function
open() has the namespace smart_door, which tells me that open() belongs to
that particular module.
Importing Within Your Project
1 | omission-git/ |
Listing 4-8: Directory structure of omission-git/
Absolute Imports
I have a class GameMode defined within the game_enums.py module, which
lives in the omission/common package. I want to use that class within my
game_round_settings.py module, defined in the omission/data package
Because I defined omission as a top-level package and organized my modules into subpackages, it’s pretty straightforward. In game_round_settings.py,
I’d write the following:
from omission.common.game_enums import GameMode
This line is an absolute import. It starts at the top-level package, omission,
and walks down into the common package
Relative Imports
You can also import from a module in the same package or subpackage.
This is known as a relative import or an intrapackage reference.
A module knows what package it belongs to, and a package knows its
parent package (if it has one). Because of this, relative imports can start the
search from the current package and move up and down through the project structure.
Within omission/data/game_round_settings.py, I can use the following
import statement:
from ..common.game_enums import GameMode
Listing 4-11: game_round_settings.py:1c
The two dots (..) mean “the current package’s direct parent package,”
which, in this case, is omission
Importing from the Same Package
In omission/data/settings.py, I have
this statement for importing a class from the module omission/data/game
_round_settings.py:
from omission.data.game_round_settings import GameRoundSettings
from game_round_settings import GameRoundSettings
However, that will not work. It will fail to locate the game_round_settings
.py module because I am running the top-level package (python3 -m omission),
and absolute imports for anything within the package being executed
(omission) have to start from the top.
use a relative import, which looks much simpler than the
absolute import:
from .game_round_settings import GameRoundSettings
Entry Points
The parts of the project that are run first when importing or
executing are called entry points
Module Entry Points
When you import a Python module or package, it is given a special variable
called __name__
. This contains the fully qualified name of the module or package, which is the name as the import system sees it. For example, the fully
qualified name of the module omission/common/game_enums.py would be
omission.common.game_enums.
when a module or package is run directly, its __name__
is set to the value “__main__
“.
a package called testpkg, which contains the module awesome.py. It defines a function, greet():
1 | def greet(): |
In another module in the same directory as testpkg, I have this module
(example.py), which I run directly with python3 example.py:
1 | from testpkg import awesome |
1 | Awesome module was run. |
what if I want that first message to appear only if awesome.py is executed directly, and not when the module is only imported? To accomplish
that, I’d check the value of __name__
variable in a conditional statement. I’ve
rewritten my awesome.py file to do exactly that:
1 | def greet(): |
Package Entry Points
omission project has a file called __main__
in the top-level
package. This file is automatically run when a package is executed directly,
but never when importing the package
So, when executing omission via python3 -m omission, Python first runs
the __init__.py
module (as always), followed by its __main__.py
module.
Otherwise, if the package is imported instead, only __init__.py
is executed.
A good __main__.py
for a top-level package would look something like
the following:
1 | def main(): |
Program Entry Points
However, you (or your eventual end user) may want to run the program
merely by double-clicking or directly executing some single Python file.
With everything else in place, this is trivial to implement.
To make my omission project easy to run, I created a single script file
outside the top-level package, named omission.py:
1 | from omission.__main__ import main |
VARIABLES AND TYPES
Variables According to Python: Names and Values
A name refers to a value or an object
. A value is a particular instance of data in memory
The term variable refers to the combination of the two: a name that refers to a value.
answer = 42
insight = answer
insight is not bound to answer, but rather to the same
value that answer was already bound to when I assigned insight. A name
always points to a value.
is
doesn’t check whether a name points to equivalent values, but rather whether
it points to the same value in memory.
When you make an assignment, Python makes its own decisions behind
the scenes about whether to create a new value in memory or bind to an
existing value
spam = 123456789
maps = spam
eggs = 123456789
print(spam == maps) # prints True
print(spam == eggs) # prints True
print(spam is maps) # prints True
print(spam is eggs) # prints False (probably)
(Interactive session)
answer = 42
insight = 42
print(answer is insight) # prints True
The is operator checks identity. Unless you really know what you’re doing,
only use this to check if something is None.>
the built-in function id() returns an integer representing the identity of whatever is passed to it.
Data Types
Names are bound to values, and those values exist in memory, as long as
there is some reference to them. You can bind a name to literally any value you
want, but you are limited as to what you can do with any particular value.
The type() Function
print(type(answer)) # prints <class 'int'>
if type(answer) is int:
print(“What’s the question?”)
use isinstance() instead of type(), as it accounts for subclasses and
inheritance
if isinstance(answer, int):
print(“What’s the question?”)
Duck Typing
If it looks like a duck, walks like a duck, and quacks like a duck,
then it probably is a duck.
Python doesn’t care much about what a value’s data type is, but rather
it cares about the functionality of the value’s data type.
If your class behaves as it
should, it usually won’t matter what it inherits from.
Scope and Garbage Collection
Scope is what defines where a variable can be accessed from. It might be
available to an entire module or limited to the suite (body) of a function.
A
name can be global, meaning it is defined by itself in a module, or it can be
local, meaning it only exists within a particular function or comprehension.
Local Scope and the Reference-Counting Garbage Collector
Functions (including lambdas) and comprehensions define their own
scope; they are the only structures in the language to do so.
When a scope reaches its end, all the names defined
within it are automatically deleted.
. Modules and
classes don’t have their own scope in the strictest sense; they only have
their own namespace.
loops don’t have their own scope.
a reference count, which is simply
a count of how many references exist for that value. Every time a value is
bound to a name, a reference is created (although there are other ways the
language may create references). When there are no more references, the
value is deleted. This is the reference-counting garbage collector, and it efficiently
handles most garbage collection scenarios.
def spam():
message = “Spam”
word = “spam”
for _ in range(100):
separator = “, “
message += separator + word
message += separator
message += “spam!”
return message
output = spam()
print(output)
even though the name message is deleted when spam() exits, the value is not.
Interpreter Shutdown
When the Python interpreter is asked to shut down, such as when a Python
program terminates, it enters interpreter shutdown. During this phase, the interpreter goes through the process of releasing all allocated resources, calling
the garbage collector multiple times, and triggering destructors in objects.
Global Scope
When a name is defined within a module but outside of any function, class,
or comprehension, it is considered to be in global scope
using
the global high_score
name. This means that anywhere I assign a value to the
name high_score in score(), the function will use the global name, instead of
trying to create a new local name.
If you’re only accessing the current
value bound to a global name, you don’t need to use the global keyword.
The Dangers of Global Scope
The nonlocal Keyword
trying to assign to a global
name will actually define a new local name that shadows the global one.
The same behavior is true of the inner function using names defined in
the outer function, which is known as the nested scope or enclosing scope.
To
get around this, I specify that eggs is nonlocal,
The nonlocal keyword starts looking for the indicated name in the innermost nested scope, and if it doesn’t find it, it moves to the next enclosing
scope above that. It repeats this until it either finds the name or determines
that the name does not exist in a nonglobal enclosing scope.
Scope Resolution
which scopes it searches for a name, and in what order,
is called the scope resolution order
the acronym LEGB
Local
Enclosing-function locals (that is, anything found via nonlocal)
Global
Built-in
The Curious Case of the Class
Every name
declared directly within a class is known as an attribute, and it is accessed
through the dot (.) operator on the class (or object) name.
The Immutable Truth
Values in Python can be either immutable or mutable. The difference hinges
on whether the values can be modified in place, meaning they can be changed
right where they are in memory.
Immutable types cannot be modified in place. For example, integers (int),
floating-point numbers (float), strings (str), and tuples (tuple) are all immutable.
If you attempt to mutate an immutable value, you’ll wind up with a
completely different value being created:
eggs = 12
carton = eggs
print(eggs is carton) # prints True
eggs += 1
print(eggs is carton) # prints False
print(eggs) # prints 13
print(carton) # prints 12
Mutable types, on the other hand, can be modified in place.
Lists constitute one example of a mutable type:
temps = [87, 76, 79]
highs = temps
print(temps is highs) # prints True
temps += [81]
print(temps is highs) # prints True
print(highs) # prints [87, 76, 79, 81]
print(temps) # prints [87, 76, 79, 81]