diff --git a/python-best-practices/README.md b/python-best-practices/README.md new file mode 100644 index 0000000000..fbe6915990 --- /dev/null +++ b/python-best-practices/README.md @@ -0,0 +1,3 @@ +# Python Best Practices: From Messy to Pythonic Code + +This folder provides the code examples for the Real Python tutorial [Python Best Practices: From Messy to Pythonic Code](https://realpython.com/python-best-practice/). diff --git a/python-best-practices/classes.py b/python-best-practices/classes.py new file mode 100644 index 0000000000..6429d8236d --- /dev/null +++ b/python-best-practices/classes.py @@ -0,0 +1,59 @@ +# Avoid this: +# class Article: +# def __init__(self, title, body, tags, db): +# self.title = title +# self.body = body +# self.tags = tags or [] +# self.db = db +# self.slug = None +# self.published = False + +# def publish(self): +# if self.slug is None: +# self.slug = "-".join(self.title.lower().split()) + +# self.db.save_article( +# title=self.title, +# body=self.body, +# tags=self.tags, +# slug=self.slug, +# ) + +# self.published = True + + +# Favor this: +from dataclasses import dataclass, field +from datetime import datetime + + +@dataclass +class Article: + title: str + body: str + tags: list[str] = field(default_factory=list) + created_at: datetime = field(default_factory=datetime.utcnow) + published_at: datetime | None = None + + @property + def is_published(self) -> bool: + return self.published_at is not None + + @property + def slug(self) -> str: + return "-".join(self.title.lower().split()) + + def __str__(self) -> str: + status = "published" if self.is_published else "draft" + return f"{self.title} [{status}]" + + +class Publisher: + def __init__(self, db): + self._db = db + + def publish(self, article: Article) -> None: + if article.is_published: + return + article.published_at = datetime.utcnow() + self._db.save(article) diff --git a/python-best-practices/code_style.py b/python-best-practices/code_style.py new file mode 100644 index 0000000000..c471125caa --- /dev/null +++ b/python-best-practices/code_style.py @@ -0,0 +1,7 @@ +# Avoid this: +# def addNum(a,b):return a+ b + + +# Favor this: +def add_numbers(a, b): + return a + b diff --git a/python-best-practices/comments.py b/python-best-practices/comments.py new file mode 100644 index 0000000000..293d258293 --- /dev/null +++ b/python-best-practices/comments.py @@ -0,0 +1,46 @@ +# Avoid this: +# def find_index(sorted_items, target): +# # Set left index +# left = 0 +# # Set right index +# right = len(sorted_items) - 1 +# # Loop while left is less than right +# while left <= right: +# # Compute middle +# mid = (left + right) // 2 +# # Check if equal +# if sorted_items[mid] == target: +# # Return mid +# return mid +# # Check if less than target +# elif sorted_items[mid] < target: +# # Move left up +# left = mid + 1 +# else: +# # Move right down +# right = mid - 1 +# # Return -1 if not found +# return -1 + + +# Favor this: +def find_index(sorted_items, target): + """Return the index of target in sorted_items, or -1 if not found.""" + left = 0 + right = len(sorted_items) - 1 + + while left <= right: + mid = (left + right) // 2 + value = sorted_items[mid] + + if value == target: + return mid + + # If target is larger, you can safely ignore the left half + if value < target: + left = mid + 1 + # Otherwise, target must be in the left half (if present) + else: + right = mid - 1 + + return -1 diff --git a/python-best-practices/comprehensions.py b/python-best-practices/comprehensions.py new file mode 100644 index 0000000000..fc47dfbe64 --- /dev/null +++ b/python-best-practices/comprehensions.py @@ -0,0 +1,8 @@ +# Avoid this: +# cubes = [] +# for number in range(10): +# cubes.append(number**3) + +# Favor this: +cubes = [number**3 for number in range(10)] +cubes diff --git a/python-best-practices/concurrency.py b/python-best-practices/concurrency.py new file mode 100644 index 0000000000..80388d12cb --- /dev/null +++ b/python-best-practices/concurrency.py @@ -0,0 +1,39 @@ +# Avoid this: +# import asyncio + +# import requests + +# async def main(): +# await asyncio.gather( +# fetch_status("https://example.com"), +# fetch_status("https://python.org"), +# ) + +# async def fetch_status(url): +# response = requests.get(url) # Blocking I/O task +# return response.status_code + +# asyncio.run(main()) + + +# Favor this: +import asyncio + +import aiohttp + + +async def main(): + async with aiohttp.ClientSession() as session: + statuses = await asyncio.gather( + fetch_status(session, "https://example.com"), + fetch_status(session, "https://realpython.com"), + ) + print(statuses) + + +async def fetch_status(session, url): + async with session.get(url) as response: # Non-blocking I/O task + return response.status + + +asyncio.run(main()) diff --git a/python-best-practices/conditionals.py b/python-best-practices/conditionals.py new file mode 100644 index 0000000000..ff0ab4afb6 --- /dev/null +++ b/python-best-practices/conditionals.py @@ -0,0 +1,41 @@ +# Avoid this: +# def shipping_cost(country, items): +# if country is not None: +# if country == "US": +# if len(items) > 0: # Non-empty cart? +# if len(items) > 10: # Free shipping? +# return 0 +# else: +# return 5 +# else: +# return 0 +# elif country == "CA": +# if len(items) > 0: # Non-empty cart? +# return 10 +# else: +# return 0 +# else: +# # Other countries +# if len(items) > 0: # Non-empty cart? +# return 20 +# else: +# return 0 +# else: +# raise ValueError("invalid country") + + +# Favor this: +def shipping_cost(country, items): + if country is None: + raise ValueError("invalid country") + + if not items: # Empty cart? + return 0 + + if country == "US": + return 0 if len(items) > 10 else 5 + + if country == "CA": + return 10 + + return 20 # Other countries diff --git a/python-best-practices/docstrings.py b/python-best-practices/docstrings.py new file mode 100644 index 0000000000..c52663334a --- /dev/null +++ b/python-best-practices/docstrings.py @@ -0,0 +1,18 @@ +# Avoid this: +# def add(a, b): +# """Return the sum of a and b.""" +# return a + b + + +# Favor this: +def add(a, b): + """Sum two numbers. + + Args: + a (int or float): The first number. + b (int or float): The second number. + + Returns: + int or float: The sum of the two numbers. + """ + return a + b diff --git a/python-best-practices/exceptions.py b/python-best-practices/exceptions.py new file mode 100644 index 0000000000..7d0106328f --- /dev/null +++ b/python-best-practices/exceptions.py @@ -0,0 +1,52 @@ +# Avoid this: +# import json + + +# def load_config(path): +# try: +# with open(path, encoding="utf-8") as config: +# data = json.load(config) +# except Exception: +# # If something went wrong, return an empty config +# return {} +# return data + + +# def main(): +# try: +# config = load_config("settings.json") +# except Exception: +# print("Sorry, something went wrong.") +# # Do something with config... + + +# Favor this: +import json +import logging + +log = logging.getLogger(__name__) + + +class ConfigError(Exception): + """Raised when issues occur with config file.""" + + +def load_config(path): + try: + with open(path, encoding="utf-8") as config: + data = json.load(config) + except FileNotFoundError as error: + raise ConfigError(f"Config file not found: {path}") from error + except json.JSONDecodeError as error: + raise ConfigError(f"Invalid JSON: {path}") from error + return data + + +def main(): + try: + config = load_config("settings.json") + print("Config:", config) + except ConfigError as error: + log.error("Error loading the config: %s", error) + print("Sorry, something went wrong while loading the settings.") + # Do something with config... diff --git a/python-best-practices/functions.py b/python-best-practices/functions.py new file mode 100644 index 0000000000..365ac12daa --- /dev/null +++ b/python-best-practices/functions.py @@ -0,0 +1,41 @@ +# Avoid this: +# import csv + +# def process_users(users, min_age, filename, send_email): +# adults = [] +# for user in users: +# if user["age"] >= min_age: +# adults.append(user) + +# with open(filename, mode="w", newline="", encoding="utf-8") as csv_file: +# writer = csv.writer(csv_file) +# writer.writerow(["name", "age"]) +# for user in adults: +# writer.writerow([user["name"], user["age"]]) + +# if send_email: +# # Emailing logic here... + +# return adults, filename + +# Favor this: +import csv + + +def filter_adult_users(users, *, min_age=18): + """Return users whose age is at least min_age.""" + return [user for user in users if user["age"] >= min_age] + + +def save_users_csv(users, filename): + """Save users to a CSV file.""" + with open(filename, mode="w", newline="", encoding="utf-8") as csv_file: + writer = csv.writer(csv_file) + writer.writerow(["name", "age"]) + for user in users: + writer.writerow([user["name"], user["age"]]) + + +def send_users_report(filename): + """Send the report.""" + # Emailing logic here... diff --git a/python-best-practices/imports.py b/python-best-practices/imports.py new file mode 100644 index 0000000000..89941272dc --- /dev/null +++ b/python-best-practices/imports.py @@ -0,0 +1,28 @@ +# Avoid this: +# from app.utils.helpers import format_date, slugify +# import requests, os, sys +# from .models import * +# import json + +# def get_data(): +# # Call the API here... + +# def build_report(data): +# # Generate report here... + + +# Favor this: +# import json +# import os +# import sys + +# import requests + +# from app.utils import helpers +# from . import models + +# def get_data(): +# # Call the API here... + +# def build_report(data): +# # Generate report here... diff --git a/python-best-practices/loggers.py b/python-best-practices/loggers.py new file mode 100644 index 0000000000..d66135d0de --- /dev/null +++ b/python-best-practices/loggers.py @@ -0,0 +1,27 @@ +# Avoid this: +# import logging + +# logging.basicConfig(level=logging.INFO) + +# def authenticate_user(username: str, password: str) -> bool: +# if username != "admin" or password != "secret": +# logging.error("Authentication failed for user %s", username) +# return False + +# logging.info("User %s authenticated successfully", username) +# return True + + +# Favor this: +import logging + +log = logging.getLogger(__name__) + + +def authenticate_user(username: str, password: str) -> bool: + if username != "admin" or password != "secret": + log.error("Authentication failed for user %s", username) + return False + + log.info("User %s authenticated successfully", username) + return True diff --git a/python-best-practices/loops.py b/python-best-practices/loops.py new file mode 100644 index 0000000000..cc656aad92 --- /dev/null +++ b/python-best-practices/loops.py @@ -0,0 +1,14 @@ +# Avoid this: +# items = ["apple", "banana", "cherry"] +# labeled = [] +# i = 0 +# while i < len(items): +# labeled.append(f"{i}: {items[i].upper()}") +# i += 1 + + +# Favor this: +items = ["apple", "banana", "cherry"] +labeled = [] +for index, item in enumerate(items): + labeled.append(f"{index}: {item.upper()}") diff --git a/python-best-practices/mutability.py b/python-best-practices/mutability.py new file mode 100644 index 0000000000..5c05670f76 --- /dev/null +++ b/python-best-practices/mutability.py @@ -0,0 +1,20 @@ +# Avoid this: +# def add_tag(tag, tags=[]): +# tags.append(tag) +# return tags + + +# add_tag("python") +# add_tag("best-practices") + + +# Favor this: +def add_tag(tag, tags=None): + if tags is None: + tags = [] + tags.append(tag) + return tags + + +add_tag("python") +add_tag("best-practices") diff --git a/python-best-practices/public_names.py b/python-best-practices/public_names.py new file mode 100644 index 0000000000..c03056f08d --- /dev/null +++ b/python-best-practices/public_names.py @@ -0,0 +1,24 @@ +# Avoid this: +# TAX_RATE = 0.20 + +# def calculate_tax(amount): +# """Return the tax for the given amount.""" +# return round_amount(amount * TAX_RATE) + +# def round_amount(amount, decimals=2): +# return round(amount, decimals) + + +# Favor this: +__all__ = ["TAX_RATE", "calculate_tax"] # Optional + +TAX_RATE = 0.20 + + +def calculate_tax(amount): + """Return the tax for the given amount.""" + return _round_amount(amount * TAX_RATE) + + +def _round_amount(amount: float, decimals=2): + return round(amount, decimals) diff --git a/python-best-practices/pythonic.py b/python-best-practices/pythonic.py new file mode 100644 index 0000000000..2479c4d961 --- /dev/null +++ b/python-best-practices/pythonic.py @@ -0,0 +1,12 @@ +# Avoid this: +# def total_length(items): +# result = 0 +# if isinstance(items, list): +# for i in range(len(items)): +# result += len(items[i]) +# return result + + +# Favor this: +def total_length(items): + return sum(len(item) for item in items) diff --git a/python-best-practices/rafactoring.py b/python-best-practices/rafactoring.py new file mode 100644 index 0000000000..87d1051bb4 --- /dev/null +++ b/python-best-practices/rafactoring.py @@ -0,0 +1,39 @@ +# Avoid this: +# def find_duplicate_emails(users): +# duplicates = [] +# seen = [] +# for user in users: +# email = user.get("email") + +# if email is None: +# print("Missing email for", user.get("id")) +# continue + +# # Check if we've seen this email before +# already_seen = False +# for index in range(len(seen)): +# if seen[index] == email: +# already_seen = True +# break + +# if already_seen: +# duplicates.append(email) +# else: +# seen.append(email) + +# print("Found", len(duplicates), "duplicate emails") +# return duplicates + + +# Favor this: +from collections import Counter + + +def _extract_emails(users): + return [user["email"] for user in users if "email" in user] + + +def find_duplicate_emails(users): + emails = _extract_emails(users) + counts = Counter(emails) + return [email for email, count in counts.items() if count > 1] diff --git a/python-best-practices/resources.py b/python-best-practices/resources.py new file mode 100644 index 0000000000..c07164f908 --- /dev/null +++ b/python-best-practices/resources.py @@ -0,0 +1,10 @@ +# Avoid this: +# def read_first_line(file_path): +# file = open(file_path, encoding="utf-8") +# return file.readline().rstrip("\n") + + +# Favor this: +def read_first_line(path): + with open(path, encoding="utf-8") as file: + return file.readline().rstrip("\n") diff --git a/python-best-practices/stdlib.py b/python-best-practices/stdlib.py new file mode 100644 index 0000000000..0418585635 --- /dev/null +++ b/python-best-practices/stdlib.py @@ -0,0 +1,16 @@ +# Avoid this: +# words = ["python", "pep8", "python", "testing"] +# counts = {} +# for word in words: +# if word in counts: +# counts[word] += 1 +# else: +# counts[word] = 1 + + +# Favor this: +from collections import Counter + +words = ["python", "pep8", "python", "testing"] +counts = Counter(words) +print(counts) diff --git a/python-best-practices/testing.py b/python-best-practices/testing.py new file mode 100644 index 0000000000..6555b3c304 --- /dev/null +++ b/python-best-practices/testing.py @@ -0,0 +1,31 @@ +# Avoid this: +# def add(a, b): +# return a + b + + +# import random + + +# def test_add(): +# a = random.randint(50, 150) +# b = random.randint(50, 150) +# result = add(a, b) +# assert 50 <= result <= 300 +# print("OK") + + +# Favor this: +# import pytest +# from calculations import add + + +# @pytest.mark.parametrize( +# "a, b, expected", +# [ +# (100, 10, 110), +# (100, 35, 135), +# (200, -50, 150), +# ], +# ) +# def test_add(a, b, expected): +# assert add(a, b) == expected diff --git a/python-best-practices/type_hint.py b/python-best-practices/type_hint.py new file mode 100644 index 0000000000..e8811b04a1 --- /dev/null +++ b/python-best-practices/type_hint.py @@ -0,0 +1,33 @@ +# Avoid this: +# import json +# from typing import Any + + +# def load_user(raw: str) -> dict[str, Any]: +# return json.loads(raw) + + +# def format_user(user: dict[str, Any]) -> str: +# return f"{user['name']} <{user['email']}>" + + +# Favor this: +import json +from typing import TypedDict + + +class UserPayload(TypedDict): + name: str + email: str + + +def load_user(raw: str) -> UserPayload: + data = json.loads(raw) + return { + "name": data["name"], + "email": data["email"], + } + + +def format_user(user: UserPayload) -> str: + return f"{user['name']} <{user['email']}>"