Технологические заметки
10 subscribers
30 photos
1 video
20 links
Пишу блог для себя.
Основной контент: парсинги, автоматизация и аналитика.
Стек технологий: Python, VBA и пр.
Download Telegram
#ТаковПуть

Фишки работы с Enum, ч.1

Enum используется для создания групп связанных констант и повышения читабельности кода.
Можно сравнить 2 примера, чтоб понять, зачем применяется Enum
result == 1
result == Results.SUCCESS

Второй пример читабельней. Т.к. в первом не понятно, что означает цифра 1.

Для работы с Enum необходимо создать класс, который будет наследоваться от Enum
Реализуем класс со статусами заказа
from enum import Enum

class OrderStatus(Enum):
CREATED = "created"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"

Интересное в таком классе то, что объекты в нем итерируются и его объекты являются экземплярами этого же класса

С итерируемостью всё просто:
for status in OrderStatus:
print(status) # возвращает объект enum
print(status.value) # возвращает значение объекта enum

Теперь давайте создадим две похожие функции.
Обе будут принимать на вход экземпляр класса OrderStatus.
Правда, возвращать они будут разные вещи: значение объекта enum, другая сам объект enum
def get_order_status_value(order_status: OrderStatus) -> str:
return order_status.value

def get_order_status_enum(order_status: OrderStatus) -> OrderStatus:
return order_status

print("delivered" == get_order_status_value(OrderStatus.DELIVERED))
print("delivered" == get_order_status_enum(OrderStatus.DELIVERED))

# Output:
# True
# False

Как видим, мы можем подать OrderStatus.DELIVERED в функцию
, т.к. OrderStatus.DELIVERED - является экземпляром класса OrderStatus
Что визуально непривычно.

Также видно, когда мы сравниваем объект со строкой, то ожидаемо получаем False

Однако. Мы можем переделать наш класс. И отнаследоваться не только от Enum, но и от str
class OrderStatus(str, Enum):
CREATED = "created"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"

Проверим, как изменились результаты сравнения:
print("delivered" == get_order_status_value(OrderStatus.DELIVERED))
print("delivered" == get_order_status_enum(OrderStatus.DELIVERED))

# Output:
# True
# True

Как видим, теперь сравнивание строки с объектом привело к True. Это связано с тем, что объект отнаследован от str.
Также отпадает необходимость в наличии функции get_order_status_value.
#ТаковПуть

Фишки работы с Enum, ч.2

Тут создадим класс, который не просто будет наследоваться от str и Enum, но и содержать различные методы.

Пояснения будут в самом коде.

from enum import Enum

class OrderStatus(str, Enum):
CREATED = "created"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"

@classmethod
def get_next_status( cls, current_status: 'OrderStatus' ) -> 'OrderStatus':
status_flow = {
cls.CREATED: cls.PROCESSING,
cls.PROCESSING: cls.SHIPPED,
cls.SHIPPED: cls.DELIVERED
}
return status_flow.get(current_status, current_status)

def is_final( self ) -> bool:
return self in {self.DELIVERED, self.CANCELLED}

@classmethod
def from_string( cls, value: str ) -> 'OrderStatus':
try:
return cls(value.lower())
except ValueError:
raise ValueError(f"Invalid status: {value}. Valid statuses are: {', '.join([s.value for s in cls])}")

# Пусть будет некий заказ, кроторый будет представлен в виде dict, и этот dict будет содержать текущий статус заказа
order = {"status": "created"}

# Проверяем статус заказа, не является ли он CREATED
print(order["status"] == OrderStatus.CREATED) # True

# Зная текущий статус, можем получить следующий
next_status = OrderStatus.get_next_status(OrderStatus.CREATED)
order["status"] = next_status.value
print(order["status"]) # "processing"

# Пробуем найти экземпляр класса OrderStatus по строке
status_enum = OrderStatus.from_string(order["status"])
print(status_enum) # OrderStatus.PROCESSING

# Проверка конечного статуса
print(OrderStatus.DELIVERED.is_final()) # True

Фактически тут представлены все те вещи, которые описывались в ч. 1, но представлены в более интересном виде.
🔥1
#ТаковПуть

Цепочка сравнений

Классический пример, где используется союз and для соединения результатов проверок:
print(1 < 5 and 5 >= 3)
# Output: True

Можно упростить конструкцию без изменения результата
print(1 < 5 >= 3)
# Output: True

Т.е. python позволяет нам писать меньше кода и дописывает конструкцию за нас (под капотом)

Но разница все же есть:
1) вторая конструкция немного медленнее (разница ничтожно мала, можно игнорировать этот факт)
2) лично для меня, первая конструкция смотрится читабельнее.
#ТаковПуть

Работа с dataclass

Читал книгу "Типизированный Python".
Там было сказано, что dataclass не распаковывается и если нужна распаковка, то лучше использовать NamedTuple

Решил показать реализацию распаковки dataclass:

from dataclasses import dataclass

@dataclass(slots=True, frozen=True)
class Coordinates:
longitude: float
latitude: float

def __iter__(self):
return iter((self.longitude, self.latitude))

@property
def coordinates(self):
return self

c = Coordinates(longitude=10, latitude=20)
lon,lat =c.coordinates

print(f"lon:{lon}")
print(f"lat:{lat}")
print(f"longitude:{c.longitude}")
# output:
# lon:10
# lat:20
# longitude:10

Распаковкой является строка:
lon,lat =c.coordinates

Это достигается реализацией метода _ _iter_ _. Плюс решил реализовать метод coordinates в виде свойства, чтоб обращаться к нему без скобок, т.е. не coordinates(), а coordinates.
#ТаковПуть

Фишки работы с Enum, ч.3

Вообще считается, что множественное наследование – это не очень хорошо. Так как при реализации таких классов могут быть конфликты между базовыми классами.

Начиная с версии 3.11 появилась возможность наследоваться от специального класса StrEnum.

from enum import StrEnum

class OrderStatus(StrEnum):
CREATED = "created"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"

#добавил, чтоб вывод от print был идентичен. На сами данные это не влияет
def __str__(self):
return f"{self.__class__.__name__}.{self.name}"

@classmethod
def get_next_status(cls, current_status: 'OrderStatus') -> 'OrderStatus':
status_flow = {
cls.CREATED: cls.PROCESSING,
cls.PROCESSING: cls.SHIPPED,
cls.SHIPPED: cls.DELIVERED
}
return status_flow.get(current_status, current_status)

def is_final(self) -> bool:
return self in {self.DELIVERED, self.CANCELLED}

@classmethod
def from_string(cls, value: str) -> 'OrderStatus':
try:
return cls(value.lower())
except ValueError:
raise ValueError(f"Invalid status: {value}. Valid statuses are: {', '.join([s.value for s in cls])}")

# Пусть будет некий заказ, который будет представлен в виде dict, и этот dict будет содержать текущий статус заказа
order = {"status": "created"}

# Проверяем статус заказа (работает напрямую со строками!)
print(order["status"] == OrderStatus.CREATED) # True

# Зная текущий статус, можем получить следующий
next_status = OrderStatus.get_next_status(OrderStatus.CREATED)
order["status"] = next_status.value
print(order["status"]) # "processing"

# Пробуем найти экземпляр класса OrderStatus по строке
status_enum = OrderStatus.from_string(order["status"])
print(status_enum) # OrderStatus.PROCESSING

# Проверка конечного статуса
print(OrderStatus.DELIVERED.is_final()) # True

Пример тот же. Множественное наследование убрано, а функционал сохранен.

Было бы скучно каждый раз писать один и тот же код. Поэтому для разнообразия пример ниже показывает, как реализовать поддержку данного кода на разных версиях python

from enum import Enum
import sys

if sys.version_info >= (3, 11):
from enum import StrEnum
BaseEnum = StrEnum
else:
class BaseEnum(str, Enum):
pass

class OrderStatus(BaseEnum):
# остальной код без изменений берем из примера выше
#ТаковПуть

Знакомимся ближе с кортежами.
fruits = ['яблоко','мандарин']
vegetables = ['лук', 'помидор']
berries = ['strawberry']
meal = (fruits, vegetables)


Как известно, кортежи неизменяемый тип данных.
Код ниже приведет к ошибке:
meal += berries

Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate tuple (not "list") to tuple

Давайте изменим неизменяемый meal!
fruits.append('апельсин')
print(meal)

(['яблоко', 'мандарин', 'апельсин'], ['лук', 'помидор'])

Попробуем еще раз изменить meal!
fruits = []
print(meal)

(['яблоко', 'мандарин', 'апельсин'], ['лук', 'помидор'])

Ой! meal не изменился! А fruits изменился?
print(fruits)

[]

fruits пустой! Но как же так, если изначально meal = (fruits, vegetables)?
Все дело в том, что fruits в начале и в конце это разные объекты.
fruits = ['яблоко','мандарин']
print(id(fruits))
fruits = []
print(id(fruits))

1140598442176 < — это id fruits = ['яблоко','мандарин']
1140594170112 < — это id fruits = []
#ТаковПуть

Удивительные кортежи: запятая против скобок

Попробуем строку (str) поместить в квадратные и круглые скобки.
Ожидаем, что l будет списком (list), а t — кортежем (tuple)

a = 'a'
l = [a] # Всё ок, это list
t = (a) # А это... что?

print(type(l)) # <class 'list'>
print(type(t)) # <class 'str'> 🤯

Но почему t стал строкой? Ожидался же кортеж!

Запись t = (a) интерпретатор видит так:
«Возьми то, что лежит в переменной a, и просто убери скобки и запиши в t».

Результат— обычная строка.

Волшебный секрет Python: кортеж создаёт не скобка, а запятая! 🤯

a = 'a'
l = [a] # Для списка запятая не нужна
t = (a,) # А для кортежа — ОБЯЗАТЕЛЬНА!

print(type(l)) # <class 'list'>
print(type(t)) # <class 'tuple'>

Скобки для кортежей часто служат для удобочитаемости. Суть — в запятой.

Вот так тоже можно:

t1 = ('a',)      # Со скобками
t2 = 'a', # Без скобок (запятая есть!)
t3 = tuple(['a']) # Явное создание

P.S. Важное исключение:
Пустой кортеж создается ТОЛЬКО скобками:

empty_tuple = ()   # Кортеж
empty_list = [] # Список
#ТаковПуть

Опасные значения по-умолчанию

Продолжаем говорить о волшебных секретах python.

Давайте попробуем указать в функции значением по-умолчанию пустой список

def add_task(task, tasks=[]):
tasks.append(task)
return tasks

morning_tasks = add_task('позавтракать')
print(morning_tasks) # ['позавтракать']

evening_tasks = add_task('поужинать')
print(evening_tasks) # ['позавтракать', 'поужинать'] 🤯

Почему вечерние задачи добавились к утренним? Мы же создали новый список!

Волшебный секрет Python: изменяемые объекты (list, set, dict) создаются в момент определения функции, а не каждого вызова! 🤯

Аргумент tasks=[] создает ОДИН общий список на все вызовы функции. Все, кто вызывает функцию без аргумента, получают ссылку на один и тот же объект.

А как же тогда сделать правильно? 🤔

Правильный путь: использовать неизменяемое значение по умолчанию (None)

def add_task(task, tasks=None):
if tasks is None:
tasks = []
tasks.append(task)
return tasks

morning_tasks = add_task('позавтракать')
print(morning_tasks) # ['позавтракать']

evening_tasks = add_task('поужинать')
print(evening_tasks) # ['поужинать']

Это правило работает для всех изменяемых типов:

· Списки - list
· Словари - dict
· Множества - set

На этом всё? А вот и нет.
Поработаем с классом:
class Task:
def __init__(self, description):
self.description = description

def create_task(desc, task_storage=Task("Дефолтная задача")):
task_storage.description = desc
return task_storage

task1 = create_task("купить хлеб")
print(task1.description) # купить хлеб

task2 = create_task("полить цветы")
print(task2.description) # полить цветы

print(task1.description) # полить цветы 🤯

Как видно, указав экземпляр класса в виде дефолтного значения, получили проблему с затиранием данных.
Затирание получилось в этом примере, на другом получилось бы дублирование.

Экземпляры классов - такие же ИЗМЕНЯЕМЫЕ объекты, как и списки!
Правило то же самое: используем None.
#ТаковПуть #ЧистыйКот

Секрет импортов (часть 1)

Обычное дело, когда одни и те же импорты используются в разных файлах проекта.

file1.py
import requests
import json
from typing import Dict
import pandas as pd

file2.py
import requests
import json
from typing import List
import pandas as pd

file3.py
import requests
import json
import pandas as pd


Логично, что если какая-то библиотека нужна в файле, просто её импортируем.
Но существует волшебный секрет 🤫
Импортированный модуль становится доступен во ВСЕМ проекте через кеш sys.modules!
Логика такая:

1. В каком-нибудь файле проекта указываем некий import
2. Находится и загружается библиотека в программу
3. Библиотека добавляется в глобальный кеш sys.modules
4. При последующих импортах в ЛЮБОМ файле просто возвращается готовый объект из кеша!!!

В программировании не рекомендуется дублировать код. В Python это относится и к импортам.

Казалось бы, проблема решена — но появляется новая!

Тут важно вспомнить дзен принцип:
The Zen of Python: 'Explicit is better than implicit'


Тут имеется в виду, что такой код, как указан ниже, тяжелее читать из-за неявного использования os.

module_a.py
import os
...

module_b.py
os.getcwd()  # Откуда взялся os?

Встает вопрос. Что делать?

Принято в каждом модуле (дублировать) указывать явно те импорты, которые в этом модуле используются.

Т.е. корректно делать так:
module_a.py
import os
...

module_b.py
import os
os.getcwd()
#ТаковПуть

Секреты импортов (часть 2)

До недавнего времени было проблемой, когда хочешь написать типизированный код на python, получается, что половина импортов – это импорты типов.

Волшебный секрет python:
Начиная с версии 3.13 типы стали полноценной частью языка!

Больше никаких импортов:

· from typing import List, Dict, Tuple
· list[int], dict[str, float], tuple[int, str]

Union и Optional ушли в историю:

· Union[str, int], Optional[str]
· str | int, str | None

Literal теперь просто значения:

· Literal['read', 'write']
· 'read' | 'write'

Self теперь не требует импорта:

· from typing import Self
· импорт не требуется

# Раньше
from typing import Self

class User:
def clone(self) -> Self:
return User()

#Теперь
class User:
def clone(self) -> Self:
return User()
👍1
#ЗаЧаем

Давно ничего не публиковал. С головой ушел в разработку программы, которая принимает на вход XML файл, а дальше:
1) предоставляет удобный интерфейс для работы с данными файла
2) генерирует отчеты по заданным полям. Потом на этот функционал надо сделать визуальную часть на Django или на чем-то подобном и сделать дружелюбный пользователю генератор отчетов. Пока все на yaml конфиге держится.
3) ради чего я это все затеял. На работе надо массово XML файлы одной структуры в XML файлы другой структуры переделывать. Можно, конечно, xslt использовать, но там есть сложные логики, какие в xslt не запихнуть.

Проект можно посмотреть тут:
https://gitverse.ru/DemonssTano/XMLProcessorAPI

Уже много чего работает, но пока прога сырая. Если есть желание поработать над реальной программой, пишите, организую :)
#ТаковПуть

Загадочные лямбды

Данную статью можно использовать как пример для собеседования.

Создадим список функций. Заранее подадим значения в функции, а потом вызовем их одну за другой.

funcs = []
items = ["a", "b", "c"]

for x in items:
funcs.append(lambda: print(x))

for func in funcs:
func()

# output:
# c
# c
# c

Что-то пошло не так...
Ожидали:a, b, c
Получили: c, c, c

Давайте усложним пример:

funcs = []
items = ["a", "b", "c"]

for x in items:
funcs.append(lambda: print(x))

x = 'd' # Изменяем значение x

for func in funcs:
func()

# output:
# d
# d
# d

Теперь вообще магия! Кажется, что "x" внутри цикла и после него - разные переменные. Проверим эту теорию с помощью id():

funcs = []
items = ["a", "b", "c"]

for x in items:
print(id(x)) # проверка id
funcs.append(lambda: print(x))

x = 'd'
print(id(x)) # проверка id
for func in funcs:
func()

Если выполнить этот код, можно убедиться - объекты x действительно разные, но результат все равно странный.

Волшебный секрет🤫

1. Лямбды "ленивые" - они вычисляются в момент вызова, а не создания
2. Замыкания (closure) в Python запоминают название переменной, а не ее значение
3. Область видимости - все лямбды ссылаются на одну и ту же переменную x

В нашем примере, при проходе циклом по items, мы перезаписываем переменную x. Когда лямбды finally вызываются, они используют текущее значение x.

Интересный поворот:

funcs = []
items = ["a", "b", "c"]

def set_funcs(items, funcs):
for x in items:
funcs.append(lambda: print(x))

set_funcs(items, funcs)

x = 'd'

for func in funcs:
func()

# output:
# c
# c
# c

Предлагаю самостоятельно подумать, почему изменился результат в примере выше

Самый простой способ исправить код: передать значение x как параметр по умолчанию:

for x in items:
funcs.append(lambda x=x: print(x))

for func in funcs:
func()
# output:
# a
# b
# c

Почему это работает:

1. Лямбды сохраняют ссылку на переменную x из внешней области видимости, а не её значение на момент создания
2. Поскольку цикл for не создаёт новую область видимости для каждой итерации, все лямбды ссылаются на одну и ту же переменную x
3. К моменту вызова лямбд значение x равно последнему элементу списка
4. Параметр по умолчанию вычисляется в момент создания функции, "замораживая" текущее значение

P.S. Эта особенность есть не только в Python — похожее поведение встречается в JavaScript и других языках с замыканиями!
#ТаковПуть

Секрет отладочного вывода f-строк

Привычная запись f-строк
result = "hello"

print(f"result='{result}'")
# output: result='hello'

Начиная с python 3.8 можно сократить запись до:
result = "hello"

print(f"{result=}")
# output: result='hello'

Самое интересное, что на этом функционал не заканчивается! У нас никуда не пропали возможности взаимодействия с объектами

name = "Alice"
age = 30
score = 95.5
items = [1, 2, 3]

print(f"{name=} {age=} {score=:.1f}")
# name='Alice' age=30 score=95.5

print(f"{name.upper()=}")
# name.upper()='ALICE'

print(f"{age * 2=}")
# age * 2=60

print(f"{items=}")
# items=[1, 2, 3]

print(f"{len(items)=}")
# len(items)=3

Но надо помнить:
Эти фишки замедляют программу, поэтому для нас это только отладочный механизм.
#ТаковПуть

Отладка без IDE

Я придерживаюсь принципа, что программист должен уметь работать без IDE.

Без IDE достаточно сложно отлаживаться, особенно в проектах с серверами, например, django.

Однако в Python есть встроенная функция для отладки, которая не требует установки IDE!

def func(x, y):
result = x + y
breakpoint() # 👈 остановка
return result * 2

func(5, 3)

Когда код дойдет до breakpoint(), выполнение остановится и откроется интерактивная консоль отладки.

В консоли отладки можно:

· n (next) - выполнить следующую строку
· c (continue) - продолжить выполнение
· s (step) - войти в функцию
· p переменная - напечатать значение переменной
· l (list) - показать код вокруг текущей строки
· q (quit) - выйти из отладчика

С примером выше мы можем сделать так:
> python func.py
> p result
8
> c

После чего отладчик завершит работу, т.к. брейкпоинтов больше нет.
👍1
#ЧистыйКот #ТаковПуть

Логирование, которое НЕ захламляет код

На курсах, в книгах, в туториалах часто говорят, что надо уходить от print в пользу logging. И дальше показывают сложные настройки логирования прямо в самом коде.

Наверно, для обучения это неплохой вариант, но в реальных программах настройки логов и бизнес логика смотрятся, неуместно, когда лежат в одном модуле.
Я говорю об этом:
import logging
# настройки логирования
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('app.log'),
logging.StreamHandler()
]
)

# бизнес логика
def business_logic():
# какие-то очень важные вычисления...

Настройки логирования можно (и нужно!) выносить из кода в отдельные файлы! Самый простой вариант это ini файл.
[loggers]
keys=root

[handlers]
keys=fileHandler,consoleHandler

[formatters]
keys=defaultFormatter

[logger_root]
level=INFO
handlers=fileHandler,consoleHandler

[handler_fileHandler]
class=FileHandler
level=INFO
formatter=defaultFormatter
args=('app.log',)

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=defaultFormatter
args=()

[formatter_defaultFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s

Файл *. ini является самым простым видом конфига, но модуль logging также поддерживает конфигурацию в формате dictionary (через logging.config.dictConfig), что часто удобнее и современнее

Считаю, что хорошим решением является вывести загрузку настроек в отдельный модуль:
# logging_config.py
import logging.config

class LoggingManager:
_configured = False

@classmethod
def get_logger(cls, name):
if not cls._configured:
cls._setup_logging()
return logging.getLogger(name)

@classmethod
def _setup_logging(cls):
logging.config.fileConfig('config.ini')
cls._configured = True

get_logger = LoggingManager.get_logger

Отдельный модуль для настроек удобен тем, что конфиг не всегда может покрыть все потребности. Например, в процессе работы программы могут появляться новые хендлеры. И в модуль можно добавить функционал добавления хендлеров и каких-то других важных вещей.

Теперь в основном коде подключение логирования будет выглядеть так:
# подключение логгера
from logging_config import get_logger
logger = get_logger(__name__)

# бизнес логика
def business_logic():
    # какие-то очень важные вычисления...

Как видно, основной код избавился от лишних строк, которые не относились к бизнес логике.

Выносите настройки логирования в конфиги — это делает код чище и профессиональнее!
#ЗаЧаем

Python взял курс на Rust.😱

Цитата из статьи:
Мы предлагаем внедрить язык программирования Rust в CPython. Сначала Rust будет использоваться только для написания дополнительных модулей расширения, но со временем он станет обязательной зависимостью CPython и будет разрешён к использованию во всей кодовой базе CPython.


Сама статья:
https://discuss.python.org/t/pre-pep-rust-for-cpython/104906
#ТаковПуть

Мост между будущим и прошлым Python!

Или как оставаться на передовой типизации, даже работая со старыми версиями Python!

Обычная ситуация для многих, когда выходит Python 3.14, а на работе 3.6, 3.7..3.10 и т.п. (нужное подчеркнуть).

И вот кодишь на работе проект на Python 3.6, а так хочется использовать современную типизацию. О которой все вокруг "трещат". И вот тут на помощь приходит...

typing_extensions

Для примера:
Self появился в Python 3.11, но благодаря typing_extensions код ниже заработает на Python 3.6

from typing_extensions import Self

class Database:
    def with_db(self) -> Self: 
        return self


А если проект написан на более новой версии Python, но необходимо его запустить на более ранней версии, то больше не надо половину кода с типизацией удалять из проекта. Эта библиотека дарит обратную совместимость типов!

Работает это так:
try:
from typing import Self
except ImportError:
from typing_extensions import Self

Т.е. просто заменяем импорт со стандартного typing на typing_extensions и (в теории) типизация из более новых версий python продолжит работать.


typing_extensions — это must-have для проектов, которые должны поддерживать старые версии Python и использовать современную типизацию!

Важно:
typing_extensions не заменяет полностью библиотеку typing, но большинство типов из новых версий Python будут работать.
#ВДзене

Долго не писал статьи в Дзене. Но, наконец, добрался до клавиатуры 😁

# 13 Индивидуальный план развития Python-разработчика

Наткнулся на статью развития фронтэнд разработчика и решил написать свой вариант, но для python-разраба.

#python #team

# 14 Telegram Bot прогноза погоды

Это копия моего предыдущего бота прогноза погоды, но на другой библиотеке. Как и прошлый бот, этот был сделан в похожем ООПэшном стиле.

#python #aiogram #TelegramBot #YandexAPI

🎯 В планах: написать опыт внедрения геймофикации, матриц компетенций и др

Предыдущие статьи.
#ТаковПуть

Библиотека pathlib в своей красе

Как и библиотека os, pathlib входит в стандартный набор библиотек для Python

Ниже прилагаю код, в котором пробегаюсь по некоторым возможностям библиотеки.
Сразу оговорюсь. Некоторые строки будут работать только в python 3.14.
from pathlib import Path

# Фиксируем расположение файла программы
program_path = Path(__file__)

# Рядом с программой будет располагаться папка data с вложенными файлами и папками
data_folder = program_path.parent / "data"
data_file = data_folder / "data.txt"
log_file = data_folder / "log.log"
archive_folder = data_folder / "archive"
archive_file = archive_folder / "archive.txt"

# Создание папки archive и всех ее родительских папок
archive_folder.mkdir(parents=True, exist_ok=True)

# Создаем файл data.txt
data_file.touch(exist_ok=True)

# Заполняем файл текстом
data_file.write_text("hello", encoding='utf-8')

# Копируем файл data.txt в папку archive
data_file.copy_into(archive_folder)

# Переименовываем файл data.txt в log.log
if not log_file.exists():
data_file.rename(log_file)

# Копируем содержимое файла log.log в data.txt
log_file.copy(data_file)

# Считываем текст файла data.txt, дополняем его и записываем в log.log
content = data_file.read_text(encoding='utf-8')
log_file.write_text(f'log: {content}')

# Удаляем все файлы .txt в папке data и ее подпапках
[file.unlink() for file in data_folder.rglob("*.txt")]


# Удаляем содержимое папки data
for item in data_folder.iterdir():
if item.is_file():
item.unlink()
elif item.is_dir() and not any(item.iterdir()):
# только пустые папки удаляются
item.rmdir()

# Удаляем пустую папку data
data_folder.rmdir()


Просто удивительно, насколько простая, лаконичная и мощная библиотека!
Когда будете очередной раз набирать import os, вспомните, что еще есть pathlib!