5692
Regular tips about Python and programming in general Owner — @pushtaev © CC BY-SA 4.0 — mention if repost
The os.curdir is a trap!import osIt's a constant indicating how the current directory is denoted in the current OS. And for all OSes that CPython supports (Windows and POSIX), it's always a dot. It might be different, though, if you run your code with MicroPython on some niche OS.
os.curdir
# '.'
Anyway, to actually get the path to the current directory, you need os.getcwd:os.getcwd()Or use pathlib:
# '/home/gram'from pathlib import Path
Path().absolute()
# PosixPath('/home/gram')
A long time ago, we already covered the chaining of comparison operations:
/channel/pythonetc/411
A quick summary is that the result of right value from each comparison gets passed into the next one:
13 > 2 > 1 # same as `13 > 2 and 2 > 1`
# True
13 > 2 > 3 # same as `13 > 2 and 2 > 3`
# False
is and in are also considered to be operators, and so can be also chained, which can lead to unexpected results:a = None
a is None # True, as expected
a is None is True # False 🤔
a is None == True # False 🤔
a is None is None # True 🤯
< and <=:teenager = 13 < age < 19Читать полностью…
The module warnings allows to print, you've guessed it, warnings. Most often, it is used to warn users of a library that the module, function, or argument they use is deprecated.
import warningsThe output:
def g():
return 2
def f():
warnings.warn(
"f is deprecated, use g instead",
DeprecationWarning,
)
return g()
f()
example.py:7: DeprecationWarning:Note that
function f is deprecated, use g instead
warnings.warn(
DeprecationWarning, as well as other warning categories, is built-in and doesn't need to be imported from anywhere.warnings.filterwarnings("error")
On the production, you can suppress warnings. Or, better, turn them into proper log records, so they will be collected wherever you collect logs:import loggingЧитать полностью…
logging.captureWarnings(True)
The method __del__ is called on the object by the garbage collector when the last reference to the object is removed:
class A:
def __del__(self):
print('destroying')
a = b = A()
del a
del b
# destroying
def f():
a = A()
f()
# destroying
file object to close the descriptor when you don't need it anymore:def f():
a_file = open('a_file.txt')
...
gc.disable().aiohttp.ClientSession uses __del__ to warn about an unclosed session:def __del__(self) -> None:Читать полностью…
if not self.closed:
warnings.warn(
f"Unclosed client session {self!r}", ResourceWarning
)
The operator is checks if the two given objects are the same object in the memory:
{} is {} # False
d = {}
d is d # True
Since types are also objects, you can use it to compare types:type(1) is int # TrueAnd you can also use
type(1) is float # False
type(1) is not float # True
== for comparing types:type(1) == int # TrueSo, when to use
is and when to use ==? There are some best practices:is to compare with None: var is None.is to compare with True and False. However, don't explicitly check for True and False in conditions, prefer just if user.admin instead of if user.admin is True. Still, the latter can be useful in tests: assert actual is True.isinstance to compare types: if isinstance(user, LoggedInUser). The big difference is that it allows subclasses. So if you have a class Admin which is subclass of LoggedInUser, it will pass isinstance check.is in some rare cases when you explicitly want to allow only the given type without subclasses: type(user) is Admin. Keep in mind, that mypy will refine the type only for isinstance but not for type is.is to compare enum members: color is Color.RED.== in ORMs and query builders like sqlalchemy: session.query(User).filter(User.admin == True). The reason is that is behavior cannot be redefined using magic methods but == can (using __eq__).== in all other cases. In particular, always use == to compare values: answer == 42.
Читать полностью…
PEP-604 (landed in Python 3.10) introduced a new short syntax for typing.Union (as I predicted, but I messed up union with intersection, shame on me):
def greet(name: str) -> str | None:
if not name:
return None
return f"Hello, {name}"
from __future__ import annotations, type checkers will understand you.
Читать полностью…
PEP-563 (landed in Python 3.7) introduced postponed evaluation of type annotations. That means, all your type annotations aren't executed at runtime but rather considered strings.
The initial idea was to make it the default behavior in Python 3.10 but it was postponed after a negative reaction from the community. In short, it would be in some cases impossible to get type information at runtime which is crucial for some tools like pydantic or typeguard. For example, see pydantic#2678.
Either way, starting from Python 3.7, you can activate this behavior by adding from __future__ import annotations at the beginning of a file. It will improve the import time and allow you to use in annotations objects that aren't defined yet.
For example:
class A:
@classmethod
def create(cls) -> A:
return cls()
Traceback (most recent call last):
File "tmp.py", line 1, in <module>
class A:
File "tmp.py", line 3, in A
def create(cls) -> A:
NameError: name 'A' is not defined
from __future__ import annotations
class A:
@classmethod
def create(cls) -> A:
return cls()
-> A: you could write -> 'A':.
Читать полностью…
The module faulthandler allows registering a handler that will dump the current stack trace in a specific file (stderr by default) upon receiving a specific signal or every N seconds.
For example, dump stack trace every 2 seconds:Output:
import faulthandler
from time import sleep
faulthandler.dump_traceback_later(
timeout=2,
repeat=True,
)
for i in range(5):
print(f"iteration {i}")
sleep(1)
iteration 0
iteration 1
Timeout (0:00:02)!
Thread 0x00007f8289147740 (most recent call first):
File "tmp.py", line 10 in <module>
iteration 2
iteration 3
Timeout (0:00:02)!
Thread 0x00007f8289147740 (most recent call first):
File "tmp.py", line 10 in <module>
iteration 4
channel = '@pythonetc'
print(f'Happy new Year, {channel}!')
# there are our top posts from 2021
by_likes = {
'join-lists': 236,
'dev-mode': 181,
'is-warning': 170,
'str-concat': 166,
'class-scope': 149,
}
by_forwards = {
'class-scope': 111,
'dev-mode': 53,
'join-lists': 50,
'str-concat': 44,
'eval-strategy': 36,
}
by_views = {
'__path__': 7_736,
'dev-mode': 7_113,
'immutable': 6_757,
'class-scope': 6_739,
'sre-parse': 6_661,
}
from datetime import date
from textwrap import dedent
if date.today().year == 2022:
print(dedent("""
The season 2.6 is coming!
This is what awaits:
"""))
print(
'native telegram reactions instead of buttons',
'deep dive into garbage collection, generators, and coroutines',
'the season is still ran by @orsinium',
'as always, guest posts and donations are welcome',
sep='\n',
)
print('See you next year \N{Sparkling Heart}!')
You are also welcome to consider joining my team. We still develop the voice assistant and beautiful devices for her to live in:
Читать полностью…
Internally, the module re uses 2 undocumented libraries:
+ sre_parse to parse regular expressions into an abstract syntax tree.
+ sre_compile to compile parsed expression.
The first one can be used to see how a regexp was parsed by Python. There are many better tools and services (like regex101.com) to debug regular expressions but this one is already in the stdlib.
>>> import sre_parseЧитать полностью…
>>> sre_parse.parse(r'([Pp]ython)\s?etc').dump()
SUBPATTERN 1 0 0
IN
LITERAL 80
LITERAL 112
LITERAL 121
LITERAL 116
LITERAL 104
LITERAL 111
LITERAL 110
MAX_REPEAT 0 1
IN
CATEGORY CATEGORY_SPACE
LITERAL 101
LITERAL 116
LITERAL 99
Python 3.7 introduced Development Mode. The mode can be activated with the -X dev argument and it makes the interpreter produce some helpful warnings. For instance:
+ Unclosed files.
+ Unawaited coroutines.
+ Unknown encoding for str.encode (by default, it is unchecked for empty strings).
+ Memory allocation issues.
$ echo 'open("/dev/null")' > tmp.py
$ python3 -X dev tmp.py
tmp.py:1: ResourceWarning: unclosed file <_io.TextIOWrapper name='/dev/null' mode='r' encoding='UTF-8'>
open("/dev/null")
ResourceWarning: Enable tracemalloc to get the object allocation traceback
Читать полностью…
Modules have a magic attribute called __path__. Whenever you're doing subpackage imports, __path__ is being searched for
that submodule. looks like a list of path strings, e.g
__path__ ["foo/bar", "/path/to/location"].
So if you do from foo import bar, or import foo.bar, foo's __path__ is being searched for bar. And if found - loaded.
You can play around with __path__ to test it out.
Create simple Python module anywhere on your system:Then, run the interpreter there and do the following:
$ tree
.
└── foo.py
$ cat foo.py
def hello():
return "hello world"As you can see,
python
>>> import os
>>> os.__path__ = ["."]
>>> from os.foo import hello
>>> hello()
'hello world' foo is now available under os:
python
>>> os.foo
<module 'os.foo' from './foo.py'>
Have you ever wondered how do relative imports work?
Im pretty sure that you've done something like that at some point:
from . import bar
from .bar import foo
__package__.foo/
__init__.py
bar/
__init__.py
main.py
__package__ for foo/__init__.py is set to "foo", and for foo/bar/__init__.py its "foo.bar".main.py __package__ isn't set, that's because main.py is not in a package.from .bar import buz within foo/__init__.py, it simply appends "bar" to foo/__init__.py's __package__ attribute, esentially it gets translated to from foo.bar import buz.__package__, e.g:>>> __package__ = "re"Читать полностью…
>>> from . import compile
>>> compile
<function compile at 0x10e0ee550>
This post is provided by @PavelDurmanov:
As you may know, generators in Python are executed step-by-step. This means that there should be a possibility to "see" that state between the steps.
All generator's local variables are stored in frame locals, and we can access the frame through the gi_frame attribute on a generator:
def gen():
x = 5
yield x
yield x
yield x
g = gen()
next(g) # 5
g.gi_frame.f_locals # {'x': 5}
g.gi_frame.f_locals["x"] = 10
next(g) # still gives us 5
import ctypes
# after we've changed the frame locals, we need to "freeze" it
# which is basically telling the interpreter to update the underlying frame based on newly added attributes
ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(g.gi_frame), ctypes.c_int(0))
next(g) # 10
ctypes.c_int(0)? There are 2 "modes" you can use to update the underlying frame, 0 and 1. If you use 1, it'll add and/or update frame local vars that are already present in the frame. So if we'd remove the x from the locals dict and call the update with c_int(0), it'd do nothing as it cannot delete the vars.c_int(1). That will replace underlying frame locals with the new locals we've defined .f_locals dict.gi_frame it's cr_frame.
Читать полностью…
The string.Template class allows to do $-style substitutions:
from string import TemplateInitially, it was introduced to simplify translations of strings. However, now PO-files natively support python-format flag. It indicates for translators that the string has
t = Template('Hello, $channel!')
t.substitute(dict(channel='@pythonetc'))
# 'Hello, @pythonetc!'
t.safe_substitute(dict())
# 'Hello, $channel!'
str.format-style substitutions. And on top of that, str.format is much more powerful and flexible.Template is to confuse newbies with one more way to format a string. Jokes aside, there are a few more cases when it can come in handy:Template.safe_substitute can be used when the template might have variables that aren't defined and should be ignored.
By using __del__ and global variables, it is possible to leave a reference to the object after it was "destroyed":
runner = None
class Lazarus:
def __del__(self):
print('destroying')
global runner
runner = self
lazarus = Lazarus()
print(lazarus)
# <__main__.Lazarus object at 0x7f853df0a790>
del lazarus
# destroying
print(runner)
# <__main__.Lazarus object at 0x7f853df0a790>
runner points to the same object as lazarus did and it's not destroyed. If you remove this reference, the object will stay in the memory forever because it's not tracked by the garbage collector anymore:del runner # it will NOT produce "destroying" message
import gc
lazarus = Lazarus()
gc.is_finalized(lazarus) # False
del lazarus
gc.is_finalized(runner) # True
The del statement is used to delete things. It has a few distinct behaviors, depending on what is the specified target.
If a variable specified, it will be removed from the scope in which it is defined:
a = []If the target has a form
del a
a
# NameError: name 'a' is not defined
target[index], target.__delitem__(index) will be called. It is defined for built-in collections to remove items from them:a = [1, 2, 3]Slices are also supported:
del a[0]
a # [2, 3]
d = {1: 2, 3: 4}
del d[3]
d # {1: 2}
a = [1, 2, 3, 4]And the last behavior, if
del a[2:]
a # [1, 2]
target.attr is specified, target.__delattr__(attr) will be called. It is defined for object:class A:Читать полностью…
b = 'default'
a = A()
a.b = 'overwritten'
a.b # 'overwritten'
del a.b
a.b # 'default'
del a.b # AttributeError
LookupError is a base class for IndexError and KeyError:
LookupError.__subclasses__()
# [IndexError, KeyError, encodings.CodecRegistryError]
KeyError.mro()
# [KeyError, LookupError, Exception, BaseException, object]
IndexError.mro()
# [IndexError, LookupError, Exception, BaseException, object]
try:Читать полностью…
username = resp['posts'][-1]['authors'][0]['name']
except LookupError:
username = None
Often, your type annotations will have circular dependencies. For example, Article has an attribute category: Category, and Category has attribute articles: list[Article]. If both classes are in the same file, adding from __future__ import annotations would solve the issue. But what if they are in different modules? Then you can hide imports that you need only for type annotations inside of the if TYPE_CHECKING block:
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .category import Category
@dataclass
class Article:
category: Category
TYPE_CHECKING = False. It won't be executed at runtime, but the type checker is a static analyzer, it doesn't care.
Читать полностью…
Now, let's see how to dump stack trace when a specific signal is received. We will use SIGUSR1 but you can do the same for any signal.
import faulthandlerNow, in a new terminal, find out the PID of the interpreter. If the file is named
from signal import SIGUSR1
from time import sleep
faulthandler.register(SIGUSR1)
sleep(60)
tmp.py, this is how you can do it (we add [] in grep to exclude the grep itself from the output):ps -ax | grep '[t]mp.py'The first number in the output is the PID. Now, use it to send the signal for PID 12345:
kill -SIGUSR1 12345And back in the terminal with the running script. You will see the stack trace:
Current thread 0x00007f22edb29740 (most recent call first):This trick can help you to see where your program has frozen without adding logs to every line. However, a better alternative can be something like py-spy which allows you to dump the current stack trace without any changes in the code. Читать полностью…
File "tmp.py", line 6 in <module>
The module atexit allows registering hooks that will be executed when the program terminates.
There are only a few cases when it is NOT executed:
+ When os._exit (don't confuse with sys.exit) is called.
+ When the interpreter failed with a fatal error.
+ When the process is hard-killed. For example, someone executed kill -9 or the system is ran out of memory.
In all other cases, like an unhandled exception or sys.exit, the registered hooks will be executed.
A few use cases:
+ Finish pending jobs
+ Send pending log messages into the log system
+ Save interactive interpreter history
However, keep in mind that there is no way to handle unhandled exceptions using atexit because it is executed after the exception is printed and discarded.Output:
import atexit
atexit.register(print, 'FINISHED')
1/0
Traceback (most recent call last):
File "example.py", line 4, in <module>
1/0
ZeroDivisionError: division by zero
FINISHED
Finally, you can express your gratitude towards the authors by donating via the new telegram donation service. Thanks and hope to see you in the third season one day.
Читать полностью…
Hi there! As you’ve probably already noticed there is no activity here at the moment. I hereby declare the second season to be officially over. I currently have no specific plan nor ideas for the third season. DM @pushtaev if you do.
For the time being, you can enjoy the archive. Posts of the channel are mostly relevant for this day.
JSON states for "JavaScript Object Notation". It's a subset of JavaScript and representation of values is based on how they are represented in JavaScript:
import json
json.dumps(1) # '1'
json.dumps(1.2) # '1.2'
json.dumps('hi') # '"hi"'
json.dumps({}) # '{}'
json.dumps([]) # '[]'
json.dumps(None) # 'null'
json.dumps(float('inf')) # 'Infinity'
json.dumps(float('nan')) # 'NaN'
inf / nan values, successfully serialized in Python, can fail deserialization in another language. For example, in Go:import "encoding/json"
func main() {
var v float64
err := json.Unmarshal(`Infinity`, &v)
println(err)
// Output: invalid character 'I' looking for beginning of value
}
allow_nan=False argument:json.dumps(float('nan'), allow_nan=False)
# ValueError: Out of range float values are not JSON compliant
Читать полностью…