New in 2026: Master Python for AI, Data Science

ProgrammingPythonPython Basic Tutorial

Python Type Hints and Annotations: From Beginner to Pro (Part 2)

Python developer using type hints and annotations with IDE showing intelligent autocomplete and type checking features

In Part 1, we explored Python’s type() function and why isinstance() is usually the better choice for runtime type checking. But here’s something that might surprise you: the most effective way to handle types in Python isn’t checking them at runtime at all—it’s using type hints to catch errors before your code even runs.

I’ve been writing Python for over a decade, and I can tell you that type hints are the single most impactful addition to Python since list comprehensions. They’ve prevented countless bugs in my production code, made my APIs self-documenting, and turned debugging sessions that used to take hours into quick fixes that my IDE catches instantly.

Yet many Python developers still avoid type hints, thinking they’re “un-Pythonic” or too complex. If you’re one of them, this guide will change your mind. You’ll learn how type hints actually make Python more Pythonic, not less, and how to use them to write better, safer code.

Why Python Added Type Hints to a Dynamic Language

Python has always been dynamically typed—you never had to declare variable types. So why did Python 3.5 introduce type hints? The answer lies in a fundamental problem that every Python developer faces as their code grows.

The Problem: Dynamic Typing at Scale

Consider this seemingly innocent function:

def calculate_discount(price, discount, customer_type):
    if customer_type == "premium":
        discount *= 1.5
    return price * (1 - discount / 100)

# How would you call this? What types do the parameters expect?
result = calculate_discount(100, 10, "premium")  # Is this right?
result = calculate_discount(100.0, 0.1, "premium")  # Or this?
result = calculate_discount("100", "10", "premium")  # What about this?

Without looking at the implementation, you have no idea:

  • Should price be an int or float?
  • Is discount a percentage (0-100) or a decimal (0.0-1.0)?
  • What are the valid values for customer_type?
  • What does the function return?

The Solution: Self-Documenting Code with Python Type Hints

Here’s the same function with type hints:

from typing import Union, Literal

def calculate_discount(
    price: Union[int, float], 
    discount: float, 
    customer_type: Literal["standard", "premium", "vip"]
) -> float:
    """Calculate discounted price.
    
    Args:
        price: Item price in dollars
        discount: Discount as decimal (0.0-1.0)
        customer_type: Customer tier affecting discount multiplier
        
    Returns:
        Final price after discount
    """
    multiplier = {"standard": 1.0, "premium": 1.5, "vip": 2.0}
    final_discount = discount * multiplier[customer_type]
    return price * (1 - final_discount)

# Now it's crystal clear how to use it
result = calculate_discount(100.0, 0.1, "premium")  # ✓ Correct usage

The type hints tell you everything you need to know at a glance. Your IDE can now provide intelligent autocomplete, catch errors before you run the code, and refactoring becomes safe and automatic.

Getting Started: Basic Type Annotations

Simple Type Annotations

The most basic type hints use Python’s built-in types:

# Basic type annotations
name: str = "Alice"
age: int = 30
height: float = 5.6
is_active: bool = True
hobbies: list = ["reading", "coding", "hiking"]
scores: dict = {"math": 95, "science": 87}

# Function annotations
def greet_user(name: str, age: int) -> str:
    return f"Hello {name}, you are {age} years old"

def calculate_area(length: float, width: float) -> float:
    return length * width

def is_adult(age: int) -> bool:
    return age >= 18

The typing Module: Beyond Built-ins

For more complex types, Python provides the typing module:

from typing import List, Dict, Set, Tuple, Optional, Union

# More specific container types
names: List[str] = ["Alice", "Bob", "Charlie"]
student_grades: Dict[str, int] = {"Alice": 95, "Bob": 87}
unique_ids: Set[int] = {1, 2, 3, 4, 5}
coordinates: Tuple[float, float] = (10.5, 20.3)

# Optional values (can be None)
middle_name: Optional[str] = None  # Same as Union[str, None]

# Union types (multiple possible types)
user_id: Union[int, str] = "user_123"  # Could be int or str

# Function with complex types
def process_user_data(
    users: List[Dict[str, Union[str, int]]], 
    active_only: bool = True
) -> Dict[str, List[str]]:
    """Process user data and return organized results."""
    result: Dict[str, List[str]] = {"active": [], "inactive": []}
    
    for user in users:
        name = user["name"]
        is_active = user.get("active", True)
        
        if active_only and is_active:
            result["active"].append(name)
        elif not active_only:
            category = "active" if is_active else "inactive"
            result[category].append(name)
    
    return result

Python 3.9+ Simplified Syntax

Python 3.9 introduced cleaner syntax using built-in types:

# Python 3.9+ style (cleaner!)
from typing import Optional, Union

# Old way (still works)
from typing import List, Dict, Tuple
names: List[str] = ["Alice", "Bob"]
grades: Dict[str, int] = {"Alice": 95}

# New way (Python 3.9+)
names: list[str] = ["Alice", "Bob"]  # Built-in list instead of typing.List
grades: dict[str, int] = {"Alice": 95}  # Built-in dict instead of typing.Dict
coordinates: tuple[float, float] = (10.5, 20.3)
user_id: int | str = "user_123"  # Union syntax using |

def get_user_scores(user_ids: list[int]) -> dict[int, Optional[int]]:
    """Get user scores, None if user not found."""
    # Implementation here...
    return {1: 95, 2: None, 3: 87}

Real-World Examples: Type Hints in Action

Web API with Type Safety

Here’s how type hints transform a typical web API:

from typing import Optional, List, Dict, Any
from dataclasses import dataclass
from datetime import datetime

@dataclass
class User:
    id: int
    username: str
    email: str
    is_active: bool
    created_at: datetime
    profile: Optional[Dict[str, Any]] = None

@dataclass
class CreateUserRequest:
    username: str
    email: str
    password: str

def create_user(request: CreateUserRequest) -> User:
    """Create a new user account."""
    # Validation happens here
    if not request.email or "@" not in request.email:
        raise ValueError("Invalid email address")
    
    if len(request.password) < 8:
        raise ValueError("Password must be at least 8 characters")
    
    # Create user (database logic would go here)
    return User(
        id=generate_user_id(),
        username=request.username,
        email=request.email,
        is_active=True,
        created_at=datetime.now()
    )

def get_users(
    active_only: bool = True,
    limit: int = 100,
    offset: int = 0
) -> List[User]:
    """Retrieve users with pagination."""
    # Database query logic here
    return fetch_users_from_db(active_only, limit, offset)

def update_user_profile(
    user_id: int, 
    profile_data: Dict[str, Any]
) -> Optional[User]:
    """Update user profile, return None if user not found."""
    user = find_user_by_id(user_id)
    if user is None:
        return None
    
    user.profile = profile_data
    save_user_to_db(user)
    return user

Data Processing Pipeline

Type hints make data processing pipelines much safer:

from typing import List, Dict, Callable, TypeVar, Generic
from pathlib import Path
import json
import csv

# Generic types for flexible data processing
T = TypeVar('T')
U = TypeVar('U')

class DataProcessor(Generic[T]):
    """Generic data processor with type safety."""
    
    def __init__(self, data: List[T]) -> None:
        self.data = data
    
    def filter(self, predicate: Callable[[T], bool]) -> 'DataProcessor[T]':
        """Filter data using predicate function."""
        filtered_data = [item for item in self.data if predicate(item)]
        return DataProcessor(filtered_data)
    
    def map(self, transform: Callable[[T], U]) -> 'DataProcessor[U]':
        """Transform data using mapping function."""
        transformed_data = [transform(item) for item in self.data]
        return DataProcessor(transformed_data)
    
    def to_list(self) -> List[T]:
        """Convert to list."""
        return self.data

# Usage with type safety
def load_sales_data(file_path: Path) -> List[Dict[str, Any]]:
    """Load sales data from CSV file."""
    sales_data: List[Dict[str, Any]] = []
    
    with open(file_path, 'r') as file:
        reader = csv.DictReader(file)
        for row in reader:
            sales_data.append({
                'date': row['date'],
                'product': row['product'],
                'amount': float(row['amount']),
                'customer_id': int(row['customer_id'])
            })
    
    return sales_data

def analyze_sales(
    data: List[Dict[str, Any]], 
    min_amount: float = 0.0
) -> Dict[str, float]:
    """Analyze sales data and return summary statistics."""
    
    processor = DataProcessor(data)
    
    # Filter high-value sales
    high_value_sales = processor.filter(
        lambda sale: sale['amount'] >= min_amount
    )
    
    # Calculate totals by product
    product_totals: Dict[str, float] = {}
    for sale in high_value_sales.to_list():
        product = sale['product']
        amount = sale['amount']
        product_totals[product] = product_totals.get(product, 0.0) + amount
    
    return product_totals

Advanced Type Hints: Professional Patterns

Protocols and Structural Typing

Protocols define interfaces without inheritance (like Go interfaces):

from typing import Protocol, runtime_checkable

@runtime_checkable
class Drawable(Protocol):
    """Anything that can be drawn."""
    def draw(self) -> str:
        ...
    
    def get_area(self) -> float:
        ...

class Circle:
    def __init__(self, radius: float) -> None:
        self.radius = radius
    
    def draw(self) -> str:
        return f"Circle with radius {self.radius}"
    
    def get_area(self) -> float:
        return 3.14159 * self.radius ** 2

class Rectangle:
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height
    
    def draw(self) -> str:
        return f"Rectangle {self.width}x{self.height}"
    
    def get_area(self) -> float:
        return self.width * self.height

def render_shapes(shapes: List[Drawable]) -> List[str]:
    """Render any drawable objects."""
    return [shape.draw() for shape in shapes]

# Both Circle and Rectangle implement Drawable protocol
shapes: List[Drawable] = [
    Circle(5.0),
    Rectangle(10.0, 8.0)
]

rendered = render_shapes(shapes)
print(rendered)  # ['Circle with radius 5.0', 'Rectangle 10.0x8.0']

Literal Python Types for Precise Values

Use Literal to specify exact allowed values:

from typing import Literal, Union

# Define exact allowed values
LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
DatabaseEngine = Literal["postgresql", "mysql", "sqlite"]
HttpMethod = Literal["GET", "POST", "PUT", "DELETE", "PATCH"]

def log_message(message: str, level: LogLevel = "INFO") -> None:
    """Log message with specific level."""
    print(f"[{level}] {message}")

def make_request(
    url: str, 
    method: HttpMethod = "GET", 
    data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
    """Make HTTP request with type-safe method."""
    # Implementation would use requests library
    return {"status": 200, "data": "response"}

class DatabaseConfig:
    def __init__(
        self, 
        engine: DatabaseEngine, 
        host: str, 
        port: int,
        database: str
    ) -> None:
        self.engine = engine
        self.host = host
        self.port = port
        self.database = database

# IDE will provide autocomplete and catch typos
log_message("Starting application", "INFO")  # ✓ Valid
# log_message("Error occurred", "FATAL")  # ✗ IDE error - not a valid LogLevel

config = DatabaseConfig("postgresql", "localhost", 5432, "myapp")  # ✓ Valid
# config = DatabaseConfig("mongodb", "localhost", 5432, "myapp")  # ✗ IDE error

Generic Types and Type Variables

Create reusable, type-safe generic functions:

from typing import TypeVar, List, Dict, Callable, Optional

T = TypeVar('T')
K = TypeVar('K')
V = TypeVar('V')

def first_or_none(items: List[T]) -> Optional[T]:
    """Get first item or None if list is empty."""
    return items[0] if items else None

def group_by(items: List[T], key_func: Callable[[T], K]) -> Dict[K, List[T]]:
    """Group items by key function result."""
    groups: Dict[K, List[T]] = {}
    for item in items:
        key = key_func(item)
        if key not in groups:
            groups[key] = []
        groups[key].append(item)
    return groups

# Usage examples with type safety
numbers = [1, 2, 3, 4, 5]
first_number = first_or_none(numbers)  # Type: Optional[int]

words = ["apple", "banana", "avocado", "blueberry"]
grouped = group_by(words, lambda word: word[0])  # Type: Dict[str, List[str]]
print(grouped)  # {'a': ['apple', 'avocado'], 'b': ['banana', 'blueberry']}

@dataclass
class Person:
    name: str
    age: int

people = [Person("Alice", 30), Person("Bob", 25), Person("Alice", 35)]
by_name = group_by(people, lambda p: p.name)  # Type: Dict[str, List[Person]]

IDE Integration and Developer Experience

Visual Studio Code Setup

Type hints truly shine when your IDE understands them. Here’s how to set up VS Code:

Extensions to Install:

  • Python (Microsoft)
  • Pylance (Microsoft’s language server)
  • mypy (optional, for additional static checking)

Settings.json configuration:

{
    "python.analysis.typeCheckingMode": "strict",
    "python.analysis.autoImportCompletions": true,
    "python.analysis.completeFunctionParens": true,
    "editor.formatOnSave": true,
    "python.formatting.provider": "black"
}

What You Get with Proper Type Hints

1. Intelligent Autocomplete

# Without type hints - IDE can't help much
def process_data(data):
    return data.  # IDE shows generic object methods

# With type hints - IDE knows exactly what's available
def process_data(data: List[Dict[str, int]]) -> int:
    return data[0].  # IDE shows dict methods: get, keys, values, etc.

2. Instant Error Detection

def calculate_tax(income: float, rate: float) -> float:
    return income * rate

# IDE immediately flags this error
result = calculate_tax("50000", 0.25)  # Error: Expected float, got str

3. Safe Refactoring

# Rename this function safely across entire codebase
def get_user_by_id(user_id: int) -> Optional[User]:
    # IDE can safely rename all references
    pass

Static Type Checking with mypy

Installing and Using mypy

pip install mypy

Basic mypy usage:

# Check single file
mypy my_script.py

# Check entire project
mypy src/

# With configuration
mypy --config-file mypy.ini src/

mypy Configuration

Create a mypy.ini file for project-wide settings:

[mypy]
python_version = 3.9
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = True
no_implicit_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
warn_no_return = True
warn_unreachable = True
strict_equality = True

# Per-module options
[mypy-requests.*]
ignore_missing_imports = True

[mypy-pandas.*]
ignore_missing_imports = True

Common mypy Errors and Solutions

Error: Incompatible return type

# This will cause mypy error
def get_username(user_id: int) -> str:
    user = find_user(user_id)
    return user.name if user else None  # Error: None is not str

# Fix with Optional
def get_username(user_id: int) -> Optional[str]:
    user = find_user(user_id)
    return user.name if user else None  # ✓ Correct

Error: Missing type annotations

# mypy error if strict mode enabled
def calculate_total(items):  # Error: Missing type annotations
    return sum(items)

# Fixed
def calculate_total(items: List[float]) -> float:
    return sum(items)

Performance: Do Type Hints Slow Down Python?

Runtime Performance Impact

import timeit
from typing import List

# Function without type hints
def sum_numbers_untyped(numbers):
    return sum(numbers)

# Function with type hints
def sum_numbers_typed(numbers: List[int]) -> int:
    return sum(numbers)

# Performance test
numbers = list(range(1000000))

# Time both functions
untyped_time = timeit.timeit(
    lambda: sum_numbers_untyped(numbers),
    number=100
)

typed_time = timeit.timeit(
    lambda: sum_numbers_typed(numbers),
    number=100
)

print(f"Untyped: {untyped_time:.4f} seconds")
print(f"Typed: {typed_time:.4f} seconds")
print(f"Difference: {abs(typed_time - untyped_time):.6f} seconds")
# Result: Virtually identical performance

The Truth About Performance:

  • Type hints are completely ignored at runtime
  • No performance impact on execution speed
  • Minimal memory overhead (annotations stored in __annotations__)
  • Static analysis tools (mypy) run separately, not during execution

Development Speed Impact

While runtime performance is unaffected, type hints dramatically improve development speed:

# Time saved debugging this without type hints: ~30 minutes
def process_user_orders(
    user: User,
    orders: List[Order],
    discount_rate: float
) -> OrderSummary:
    """Process user orders with discount."""
    # With type hints, IDE catches:
    # - Wrong attribute access (user.nme instead of user.name)
    # - Wrong method calls (orders.appnd instead of orders.append)  
    # - Type mismatches (passing string instead of float)
    
    total = 0.0
    for order in orders:
        total += order.amount * (1 - discount_rate)
    
    return OrderSummary(
        user_id=user.id,
        total_amount=total,
        order_count=len(orders)
    )

Migration Strategy: Adding Types to Existing Code

Gradual Adoption Approach

You don’t need to add types everywhere at once. Start strategically:

Phase 1: Public APIs

# Start with function signatures that other code depends on
def create_user(username: str, email: str) -> User:
    pass

def get_user_orders(user_id: int) -> List[Order]:
    pass

Phase 2: Data Models

# Add types to your data classes/models
@dataclass
class Product:
    id: int
    name: str
    price: float
    category_id: int

Phase 3: Internal Functions

# Gradually add types to internal functions
def _validate_email(email: str) -> bool:
    return "@" in email and "." in email

def _calculate_shipping(weight: float, distance: float) -> float:
    return weight * 0.1 + distance * 0.05

Dealing with Legacy Code

Use Any type temporarily for complex legacy interactions:

from typing import Any, cast

def process_legacy_data(legacy_obj: Any) -> Dict[str, str]:
    """Process data from legacy system."""
    # Cast when you know the type but mypy doesn't
    typed_obj = cast(Dict[str, Any], legacy_obj)
    
    return {
        "name": str(typed_obj["name"]),
        "status": str(typed_obj["status"])
    }

Common Pitfalls and How to Avoid Them

Pitfall 1: Overusing Any

# Don't do this - defeats the purpose
def process_data(data: Any) -> Any:
    return data.do_something()

# Do this - be as specific as possible
def process_data(data: Dict[str, Union[str, int]]) -> List[str]:
    return [str(value) for value in data.values()]

Pitfall 2: Missing Optional Types

# Wrong - can return None but type says str
def get_config_value(key: str) -> str:
    return config.get(key)  # get() can return None!

# Correct - acknowledge None possibility
def get_config_value(key: str) -> Optional[str]:
    return config.get(key)

Pitfall 3: Mutable Default Arguments

# Wrong - dangerous mutable default
def add_item(items: List[str] = []) -> List[str]:
    items.append("new item")
    return items

# Correct - use Optional with None
def add_item(items: Optional[List[str]] = None) -> List[str]:
    if items is None:
        items = []
    items.append("new item")
    return items

Type Hints in Different Python Contexts

Web Frameworks Integration

FastAPI with Type Hints:

from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional

app = FastAPI()

class User(BaseModel):
    id: int
    username: str
    email: str
    is_active: bool = True

class CreateUser(BaseModel):
    username: str
    email: str
    password: str

@app.post("/users/", response_model=User)
async def create_user(user: CreateUser) -> User:
    # FastAPI automatically validates types and generates docs
    return User(
        id=generate_id(),
        username=user.username,
        email=user.email
    )

@app.get("/users/", response_model=List[User])
async def get_users(active: bool = True) -> List[User]:
    return fetch_users(active_only=active)

Django with Type Hints:

from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404
from typing import Dict, Any
from .models import User

def get_user_profile(request: HttpRequest, user_id: int) -> JsonResponse:
    """Get user profile as JSON."""
    user: User = get_object_or_404(User, id=user_id)
    
    profile_data: Dict[str, Any] = {
        "id": user.id,
        "username": user.username,
        "email": user.email,
        "date_joined": user.date_joined.isoformat()
    }
    
    return JsonResponse(profile_data)

Data Science with Type Hints

import pandas as pd
import numpy as np
from typing import Tuple, List

def clean_dataframe(
    df: pd.DataFrame, 
    required_columns: List[str]
) -> pd.DataFrame:
    """Clean dataframe by removing rows with missing required data."""
    return df.dropna(subset=required_columns)

def calculate_statistics(
    data: np.ndarray
) -> Tuple[float, float, float]:
    """Calculate mean, std, and median."""
    return float(np.mean(data)), float(np.std(data)), float(np.median(data))

def process_sales_data(
    sales_file: str
) -> Dict[str, float]:
    """Process sales data and return summary."""
    df = pd.read_csv(sales_file)
    df = clean_dataframe(df, ["amount", "date", "customer_id"])
    
    amounts: np.ndarray = df["amount"].values
    mean, std, median = calculate_statistics(amounts)
    
    return {
        "total_sales": float(df["amount"].sum()),
        "average_sale": mean,
        "std_dev": std,
        "median_sale": median
    }

Testing with Type Hints

Type-Safe Test Functions

import pytest
from typing import List, Dict, Any

def test_user_creation() -> None:
    """Test user creation with type safety."""
    user_data: Dict[str, Any] = {
        "username": "testuser",
        "email": "[email protected]",
        "age": 25
    }
    
    user: User = create_user(user_data)
    
    assert user.username == "testuser"
    assert user.email == "[email protected]"
    assert user.age == 25

@pytest.mark.parametrize("test_input,expected", [
    ([1, 2, 3], 6),
    ([10, 20], 30),
    ([], 0),
])
def test_sum_numbers(test_input: List[int], expected: int) -> None:
    """Test sum function with various inputs."""
    result: int = sum_numbers(test_input)
    assert result == expected

Best Practices and Professional Tips

1. Start Simple, Get Specific

# Start with basic types
def process_data(data: list) -> dict:
    pass

# Evolve to more specific types
def process_data(data: List[Dict[str, Union[str, int]]]) -> Dict[str, Any]:
    pass

# End with precise, domain-specific types
def process_user_data(
    users: List[UserData]
) -> UserProcessingResult:
    pass

2. Use Type Aliases for Complex Types

from typing import Dict, List, Union

# Create readable aliases
UserId = int
UserData = Dict[str, Union[str, int, bool]]
UserList = List[UserData]
ProcessingResult = Dict[str, Union[int, List[str]]]

def process_users(users: UserList) -> ProcessingResult:
    """Much more readable than the raw types."""
    pass

3. Document Edge Cases

def divide_numbers(a: float, b: float) -> float:
    """Divide two numbers.
    
    Args:
        a: Dividend
        b: Divisor (must not be zero)
        
    Returns:
        Result of a/b
        
    Raises:
        ZeroDivisionError: When b is zero
    """
    if b == 0:
        raise ZeroDivisionError("Cannot divide by zero")
    return a / b

Future of Python Typing

Python 3.10+ Features

Union Types with | (Python 3.10+):

# Old way
from typing import Union
def process_id(user_id: Union[int, str]) -> str:
    return str(user_id)

# New way (Python 3.10+)
def process_id(user_id: int | str) -> str:
    return str(user_id)

Pattern Matching with Types (Python 3.10+):

def process_response(response: dict[str, Any]) -> str:
    match response:
        case {"status": "success", "data": data}:
            return f"Success: {data}"
        case {"status": "error", "message": msg}:
            return f"Error: {msg}"
        case _:
            return "Unknown response format"

Key Takeaways

  • Type hints make Python more maintainable, not less Pythonic
  • Start with public APIs and gradually add types to internal functions
  • Use your IDE’s type checking for immediate feedback
  • mypy provides additional static analysis beyond IDE checking
  • Performance impact is zero – types are purely for development
  • Gradual adoption works – you don’t need to type everything at once

Type hints represent the evolution of Python from a scripting language to a platform for building large, maintainable applications. They preserve Python’s dynamic nature while adding the safety and developer experience of static typing.

What’s Next in This Series

In Part 3: “Python’s Duck Typing vs Static Typing: When to Use Each”, we’ll explore:

  • The philosophy behind Python’s dynamic typing
  • When duck typing is still the best choice
  • Performance implications of different typing approaches
  • Design patterns that work best with each approach
  • Real-world case studies of typing decisions

Armed with practical type hints knowledge, you’ll be ready to make informed decisions about when to use Python’s flexibility and when to add static typing constraints.

Have you started using type hints in your projects? Share your experience in the comments—what benefits have you seen, and what challenges have you encountered? Your insights help other developers make the transition to typed Python!

Coming next week: Part 3 – Python’s Duck Typing vs Static Typing: When to Use Each where we’ll dive deep into Python’s typing philosophy and decision-making frameworks.

External Links

Tags: Python, Type Hints, Type Annotations, typing Module, mypy, Static Analysis, Python Programming, Code Quality, IDE Integration, Software Development, Python, FastAPI, Django

Related posts
Python

Pydantic Agent Basics: A Complete 2026 Tutorial

ProgrammingPython

Production-Ready MCP Servers — Security, Testing & Deployment

ProgrammingPython

Build Your First MCP Server with Python SDK — Fundamentals

ProgrammingPython

Connect FastAPI to MCP — Two Integration Patterns

Leave a Reply