Documentation Writer
Overview
The Documentation Writer skill produces clear, accurate, and maintainable technical documentation across all layers: project-level READMEs, module-level docstrings, function-level API docs, and inline code comments. It covers the standard formats for multiple languages (JSDoc, Python docstrings in Google/NumPy/Sphinx style, GoDoc), README structure best practices, CHANGELOG format (Keep a Changelog), and principles for writing comments that add value rather than noise. Good documentation is part of the code — it reduces onboarding time, prevents misuse, and serves as a contract with consumers.
When to Use
- Writing a README for a new library, tool, or service
- Adding docstrings to functions, classes, or modules
- Documenting a public API for external or internal consumers
- Writing or updating a CHANGELOG when releasing a new version
- Adding inline comments to explain non-obvious logic
When NOT to Use
- Writing user-facing help docs or tutorials (different audience and tone)
- Writing blog posts or marketing copy
- Generating API reference docs from non-annotated code (annotate first, then generate)
- Writing architectural decisions (use the architecture-designer skill for ADRs)
Quick Reference
| Doc Type |
Purpose |
Format |
| README |
Project overview, quickstart, usage |
Markdown with badges, code blocks, TOC |
| Docstring (Python) |
Function/class contract |
Google, NumPy, or Sphinx style |
| JSDoc |
JS/TS function/class docs |
/** @param @returns @throws */ |
| GoDoc |
Go package/function docs |
Plain comment above declaration |
| Inline comment |
Explain why, not what |
Sparingly; 1-2 lines |
| CHANGELOG |
User-facing change history |
Keep a Changelog format |
| OpenAPI |
REST API reference |
YAML/JSON, generated from annotations |
Instructions
Know your audience
- README: New users who need to evaluate, install, and get started quickly.
- Docstrings: Developers who will call your function — they need to know the contract (parameters, return values, exceptions, side effects), not the implementation.
- Inline comments: The developer maintaining this code 6 months from now (possibly you).
- CHANGELOG: Users upgrading — they need to know what changed and if they need to do anything.
Write the README with this structure
1. Project name + one-line tagline
2. Badges (CI, coverage, version, license)
3. Short description (2-3 sentences, why does this exist?)
4. Quick Start / Installation (fewest steps to get running)
5. Usage with code examples
6. Configuration reference
7. API reference (or link to generated docs)
8. Contributing guide
9. License
Write docstrings for every public function, class, and module
- Describe what it does, not how (don't repeat the code).
- Document every parameter with type and description.
- Document the return value with type and description.
- Document every exception that can be raised and when.
- Include a usage example for non-trivial functions.
Write inline comments that explain why, not what
- Bad:
i += 1 # increment i (the code already says this)
- Good:
# Skip the header row — it's not data
- Good:
# Retry up to 3 times to handle transient network errors
- Comment non-obvious algorithms, workarounds, and performance-critical decisions.
Maintain the CHANGELOG
- Follow Keep a Changelog:
Added, Changed, Deprecated, Removed, Fixed, Security.
- Each release has a version number and date.
- Write from the user's perspective: "what changed for me?" not "what did we do?"
Review and update docs with code changes
- Treat outdated documentation as a bug.
- Update docstrings when function signatures, behavior, or exceptions change.
- Update the README when installation steps, config options, or usage patterns change.
Examples
Example 1: Write a README for an open-source Python library
Input: "Write a README for quickcache, a Python library that provides a simple decorator for caching function results with TTL support."
Output:
# quickcache
[](https://github.com/example/quickcache/actions)
[](https://pypi.org/project/quickcache/)
[](https://codecov.io/gh/example/quickcache)
[](LICENSE)
A simple, zero-dependency Python decorator for caching function results with optional time-to-live (TTL) expiration. Stop re-computing expensive results on every call.
## Quick Start
```bash
pip install quickcache
```
```python
from quickcache import cache
@cache(ttl=60) # Cache for 60 seconds
def get_user(user_id: int) -> dict:
return database.fetch_user(user_id)
user = get_user(42) # Hits the database
user = get_user(42) # Returns cached result instantly
```
## Features
- ✅ Simple decorator API
- ✅ Configurable TTL (time-to-live) per function
- ✅ Supports sync and async functions
- ✅ Thread-safe in-memory cache
- ✅ Optional Redis backend for distributed caching
- ✅ Zero dependencies (in-memory mode)
## Installation
```bash
pip install quickcache # In-memory caching (zero dependencies)
pip install quickcache[redis] # Redis backend
```
## Usage
### Basic caching
```python
from quickcache import cache
@cache()
def expensive_computation(x: int, y: int) -> int:
"""Cached with no TTL — cached until process restarts."""
return x ** y
```
### With TTL
```python
@cache(ttl=300) # Cache for 5 minutes
def fetch_exchange_rate(currency: str) -> float:
return api.get_rate(currency)
```
### Async support
```python
@cache(ttl=60)
async def fetch_user_async(user_id: int) -> dict:
return await db.fetch_user(user_id)
```
### Redis backend (distributed)
```python
from quickcache import cache
from quickcache.backends import RedisBackend
backend = RedisBackend(host='localhost', port=6379, db=0)
@cache(ttl=120, backend=backend)
def get_product(product_id: int) -> dict:
return db.fetch_product(product_id)
```
### Cache invalidation
```python
from quickcache import cache, invalidate
@cache(ttl=60, key='user:{user_id}')
def get_user(user_id: int) -> dict:
return db.fetch_user(user_id)
# Invalidate a specific user's cache
invalidate('user:42')
# Invalidate by function
get_user.invalidate(42)
# Clear all cache
get_user.clear()
```
## Configuration
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `ttl` | `int \| None` | `None` | Seconds before cache entry expires. `None` = never expires. |
| `backend` | `Backend` | `MemoryBackend()` | Cache backend instance. |
| `key` | `str \| None` | Auto | Cache key template. Use `{param_name}` for dynamic keys. |
| `maxsize` | `int` | `1024` | Max entries for in-memory backend (LRU eviction). |
## Contributing
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for the development setup, coding standards, and pull request process.
```bash
git clone https://github.com/example/quickcache.git
cd quickcache
pip install -e ".[dev]"
pytest
```
## License
MIT — see [LICENSE](LICENSE).
Example 2: Document a Python module with docstrings
Input: A Python module to document:
import re
from typing import Optional
def validate_email(email, strict=False):
pattern = r'^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$'
if strict:
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email, re.IGNORECASE))
def normalize_email(email):
local, domain = email.rsplit('@', 1)
local = local.split('+')[0]
return f"{local.lower()}@{domain.lower()}"
class EmailValidator:
def __init__(self, allowed_domains=None):
self.allowed_domains = set(allowed_domains or [])
def is_valid(self, email):
if not validate_email(email):
return False
if self.allowed_domains:
domain = email.rsplit('@', 1)[1].lower()
return domain in self.allowed_domains
return True
Output (with Google-style docstrings):
"""Email validation and normalization utilities.
This module provides functions and a class for validating and normalizing
email addresses. It supports basic RFC-compliant validation, strict mode
for stricter character rules, and domain allowlisting.
Typical usage::
from email_utils import validate_email, normalize_email, EmailValidator
if validate_email("alice@example.com"):
canonical = normalize_email("Alice+tag@Example.COM")
# canonical == "alice@example.com"
validator = EmailValidator(allowed_domains=["company.com"])
validator.is_valid("alice@company.com") # True
validator.is_valid("alice@gmail.com") # False
"""
import re
from typing import Optional
def validate_email(email: str, strict: bool = False) -> bool:
"""Validate whether a string is a well-formed email address.
Uses a regex pattern to check that the email has a valid local part,
an @ symbol, a domain, and a TLD of at least 2 characters. Strict mode
disallows some special characters (e.g., `!`, `#`) in the local part.
Args:
email: The email address string to validate.
strict: If True, applies stricter character rules in the local part
(only alphanumeric, dots, underscores, percent, plus, hyphen).
Defaults to False.
Returns:
True if the email matches the pattern; False otherwise.
Examples:
>>> validate_email("alice@example.com")
True
>>> validate_email("not-an-email")
False
>>> validate_email("alice!@example.com", strict=True)
False
"""
pattern = r'^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$'
if strict:
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email, re.IGNORECASE))
def normalize_email(email: str) -> str:
"""Normalize an email address to its canonical form.
Strips the plus-addressing suffix from the local part and lowercases
both the local part and the domain. This enables deduplication of
addresses like ``Alice+newsletter@Example.COM`` and ``alice@example.com``.
Note:
This function does not validate the email. Call ``validate_email``
first if you need to ensure the input is well-formed.
Args:
email: A well-formed email address string.
Returns:
The normalized email address in lowercase with plus-addressing removed.
Raises:
ValueError: If the email does not contain an ``@`` symbol.
Examples:
>>> normalize_email("Alice+tag@Example.COM")
'alice@example.com'
>>> normalize_email("BOB@COMPANY.ORG")
'bob@company.org'
"""
if '@' not in email:
raise ValueError(f"Invalid email address (missing @): {email!r}")
local, domain = email.rsplit('@', 1)
local = local.split('+')[0]
return f"{local.lower()}@{domain.lower()}"
class EmailValidator:
"""Validates email addresses with optional domain allowlisting.
Combines format validation with domain restriction. Useful for
ensuring that only users with addresses from specific domains
(e.g., corporate email) can register.
Attributes:
allowed_domains: A set of lowercase domain strings that are
permitted. Empty set means all domains are allowed.
Example:
>>> validator = EmailValidator(allowed_domains=["company.com", "company.org"])
>>> validator.is_valid("alice@company.com")
True
>>> validator.is_valid("bob@gmail.com")
False
>>> open_validator = EmailValidator()
>>> open_validator.is_valid("anyone@anywhere.io")
True
"""
def __init__(self, allowed_domains: Optional[list[str]] = None) -> None:
"""Initialize the validator with an optional domain allowlist.
Args:
allowed_domains: A list of permitted email domains (case-insensitive).
If None or empty, all domains are allowed.
"""
self.allowed_domains = {d.lower() for d in (allowed_domains or [])}
def is_valid(self, email: str) -> bool:
"""Check whether an email address is valid and from an allowed domain.
Args:
email: The email address to validate.
Returns:
True if the email passes format validation and (if configured)
its domain is in the allowlist. False otherwise.
"""
if not validate_email(email):
return False
if self.allowed_domains:
domain = email.rsplit('@', 1)[1].lower()
return domain in self.allowed_domains
return True
JSDoc equivalent (TypeScript):
/**
* Validates whether a string is a well-formed email address.
*
* @param email - The email address string to validate.
* @param strict - If true, applies stricter character rules in the local part.
* @returns `true` if the email matches the expected pattern; `false` otherwise.
*
* @example
* ```ts
* validateEmail("alice@example.com"); // true
* validateEmail("not-an-email"); // false
* validateEmail("alice!@example.com", true); // false (strict mode)
* ```
*/
export function validateEmail(email: string, strict = false): boolean {
const pattern = strict
? /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/i
: /^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$/i;
return pattern.test(email);
}
GoDoc equivalent:
// Package email provides utilities for validating and normalizing email addresses.
//
// Example usage:
//
// if email.IsValid("alice@example.com") {
// canonical := email.Normalize("Alice+tag@Example.COM")
// fmt.Println(canonical) // alice@example.com
// }
package email
// IsValid reports whether s is a well-formed email address.
// It uses a regex-based heuristic and is suitable for most real-world
// validation purposes. For strict RFC 5321 compliance, use IsValidStrict.
func IsValid(s string) bool { ... }
// Normalize returns the canonical form of the email address by lowercasing
// both the local part and domain, and stripping any plus-addressing suffix
// (e.g., "Alice+tag@Example.COM" → "alice@example.com").
//
// Normalize does not validate the email. Call IsValid first if needed.
func Normalize(s string) (string, error) { ... }
Best Practices
- Write docs before or alongside code, not as an afterthought
- Keep docs close to the code they describe — co-located docstrings beat separate wiki pages
- Treat documentation as code: review it in PRs, update it with behavior changes
- Use examples liberally — a usage example is worth more than three paragraphs of prose
- Make the first sentence of a docstring a complete, standalone description (it appears in IDE tooltips and auto-generated indexes)
Common Mistakes
- Restating what the code does ("This function increments x by 1") instead of explaining purpose and contract
- Documenting parameters without types or units (is
timeout in ms or seconds?)
- Outdated docs that contradict the current behavior — worse than no docs
- Missing exception documentation — callers can't handle errors they don't know about
- Documenting private/internal implementation details that may change vs. the stable public API
Tips & Tricks
pydoc, sphinx, typedoc, and godoc generate HTML API references from docstrings automatically
- Keep a Changelog (keepachangelog.com) is the standard CHANGELOG format — follow it consistently
- Use
# TODO(username): reason format for deferred work so it's trackable
- Add examples to docstrings that are also run as doctests in Python:
pytest --doctest-modules
- Badge generators: shields.io creates badges for README (CI status, version, license, coverage)
Related Skills
1---2name: documentation-writer3description: Use this skill when writing or improving technical documentation including READMEs, docstrings, API docs, changelogs, or inline code comments. Trigger phrases: 'write a README', 'document this function', 'add docstrings', 'improve the docs'. Not for writing blog posts, marketing copy, or user-facing help articles.4license: MIT5---67# Documentation Writer89## Overview10The Documentation Writer skill produces clear, accurate, and maintainable technical documentation across all layers: project-level READMEs, module-level docstrings, function-level API docs, and inline code comments. It covers the standard formats for multiple languages (JSDoc, Python docstrings in Google/NumPy/Sphinx style, GoDoc), README structure best practices, CHANGELOG format (Keep a Changelog), and principles for writing comments that add value rather than noise. Good documentation is part of the code — it reduces onboarding time, prevents misuse, and serves as a contract with consumers.1112## When to Use13- Writing a README for a new library, tool, or service14- Adding docstrings to functions, classes, or modules15- Documenting a public API for external or internal consumers16- Writing or updating a CHANGELOG when releasing a new version17- Adding inline comments to explain non-obvious logic1819## When NOT to Use20- Writing user-facing help docs or tutorials (different audience and tone)21- Writing blog posts or marketing copy22- Generating API reference docs from non-annotated code (annotate first, then generate)23- Writing architectural decisions (use the architecture-designer skill for ADRs)2425## Quick Reference26| Doc Type | Purpose | Format |27|----------|---------|--------|28| README | Project overview, quickstart, usage | Markdown with badges, code blocks, TOC |29| Docstring (Python) | Function/class contract | Google, NumPy, or Sphinx style |30| JSDoc | JS/TS function/class docs | `/** @param @returns @throws */` |31| GoDoc | Go package/function docs | Plain comment above declaration |32| Inline comment | Explain *why*, not *what* | Sparingly; 1-2 lines |33| CHANGELOG | User-facing change history | Keep a Changelog format |34| OpenAPI | REST API reference | YAML/JSON, generated from annotations |3536## Instructions37381. **Know your audience**39 - **README**: New users who need to evaluate, install, and get started quickly.40 - **Docstrings**: Developers who will call your function — they need to know the contract (parameters, return values, exceptions, side effects), not the implementation.41 - **Inline comments**: The developer maintaining this code 6 months from now (possibly you).42 - **CHANGELOG**: Users upgrading — they need to know what changed and if they need to do anything.43442. **Write the README with this structure**45 ```46 1. Project name + one-line tagline47 2. Badges (CI, coverage, version, license)48 3. Short description (2-3 sentences, why does this exist?)49 4. Quick Start / Installation (fewest steps to get running)50 5. Usage with code examples51 6. Configuration reference52 7. API reference (or link to generated docs)53 8. Contributing guide54 9. License55 ```56573. **Write docstrings for every public function, class, and module**58 - Describe *what* it does, not *how* (don't repeat the code).59 - Document every parameter with type and description.60 - Document the return value with type and description.61 - Document every exception that can be raised and when.62 - Include a usage example for non-trivial functions.63644. **Write inline comments that explain *why*, not *what***65 - Bad: `i += 1 # increment i` (the code already says this)66 - Good: `# Skip the header row — it's not data`67 - Good: `# Retry up to 3 times to handle transient network errors`68 - Comment non-obvious algorithms, workarounds, and performance-critical decisions.69705. **Maintain the CHANGELOG**71 - Follow Keep a Changelog: `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`.72 - Each release has a version number and date.73 - Write from the user's perspective: "what changed for me?" not "what did we do?"74756. **Review and update docs with code changes**76 - Treat outdated documentation as a bug.77 - Update docstrings when function signatures, behavior, or exceptions change.78 - Update the README when installation steps, config options, or usage patterns change.7980## Examples8182### Example 1: Write a README for an open-source Python library8384**Input:** "Write a README for `quickcache`, a Python library that provides a simple decorator for caching function results with TTL support."8586**Output:**8788````markdown89# quickcache9091[](https://github.com/example/quickcache/actions)92[](https://pypi.org/project/quickcache/)93[](https://codecov.io/gh/example/quickcache)94[](LICENSE)9596A simple, zero-dependency Python decorator for caching function results with optional time-to-live (TTL) expiration. Stop re-computing expensive results on every call.9798## Quick Start99100```bash101pip install quickcache102```103104```python105from quickcache import cache106107@cache(ttl=60) # Cache for 60 seconds108def get_user(user_id: int) -> dict:109 return database.fetch_user(user_id)110111user = get_user(42) # Hits the database112user = get_user(42) # Returns cached result instantly113```114115## Features116117- ✅ Simple decorator API118- ✅ Configurable TTL (time-to-live) per function119- ✅ Supports sync and async functions120- ✅ Thread-safe in-memory cache121- ✅ Optional Redis backend for distributed caching122- ✅ Zero dependencies (in-memory mode)123124## Installation125126```bash127pip install quickcache # In-memory caching (zero dependencies)128pip install quickcache[redis] # Redis backend129```130131## Usage132133### Basic caching134135```python136from quickcache import cache137138@cache()139def expensive_computation(x: int, y: int) -> int:140 """Cached with no TTL — cached until process restarts."""141 return x ** y142```143144### With TTL145146```python147@cache(ttl=300) # Cache for 5 minutes148def fetch_exchange_rate(currency: str) -> float:149 return api.get_rate(currency)150```151152### Async support153154```python155@cache(ttl=60)156async def fetch_user_async(user_id: int) -> dict:157 return await db.fetch_user(user_id)158```159160### Redis backend (distributed)161162```python163from quickcache import cache164from quickcache.backends import RedisBackend165166backend = RedisBackend(host='localhost', port=6379, db=0)167168@cache(ttl=120, backend=backend)169def get_product(product_id: int) -> dict:170 return db.fetch_product(product_id)171```172173### Cache invalidation174175```python176from quickcache import cache, invalidate177178@cache(ttl=60, key='user:{user_id}')179def get_user(user_id: int) -> dict:180 return db.fetch_user(user_id)181182# Invalidate a specific user's cache183invalidate('user:42')184185# Invalidate by function186get_user.invalidate(42)187188# Clear all cache189get_user.clear()190```191192## Configuration193194| Parameter | Type | Default | Description |195|-----------|------|---------|-------------|196| `ttl` | `int \| None` | `None` | Seconds before cache entry expires. `None` = never expires. |197| `backend` | `Backend` | `MemoryBackend()` | Cache backend instance. |198| `key` | `str \| None` | Auto | Cache key template. Use `{param_name}` for dynamic keys. |199| `maxsize` | `int` | `1024` | Max entries for in-memory backend (LRU eviction). |200201## Contributing202203Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for the development setup, coding standards, and pull request process.204205```bash206git clone https://github.com/example/quickcache.git207cd quickcache208pip install -e ".[dev]"209pytest210```211212## License213214MIT — see [LICENSE](LICENSE).215````216217---218219### Example 2: Document a Python module with docstrings220221**Input:** A Python module to document:222```python223import re224from typing import Optional225226def validate_email(email, strict=False):227 pattern = r'^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$'228 if strict:229 pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'230 return bool(re.match(pattern, email, re.IGNORECASE))231232def normalize_email(email):233 local, domain = email.rsplit('@', 1)234 local = local.split('+')[0]235 return f"{local.lower()}@{domain.lower()}"236237class EmailValidator:238 def __init__(self, allowed_domains=None):239 self.allowed_domains = set(allowed_domains or [])240241 def is_valid(self, email):242 if not validate_email(email):243 return False244 if self.allowed_domains:245 domain = email.rsplit('@', 1)[1].lower()246 return domain in self.allowed_domains247 return True248```249250**Output (with Google-style docstrings):**251```python252"""Email validation and normalization utilities.253254This module provides functions and a class for validating and normalizing255email addresses. It supports basic RFC-compliant validation, strict mode256for stricter character rules, and domain allowlisting.257258Typical usage::259260 from email_utils import validate_email, normalize_email, EmailValidator261262 if validate_email("alice@example.com"):263 canonical = normalize_email("Alice+tag@Example.COM")264 # canonical == "alice@example.com"265266 validator = EmailValidator(allowed_domains=["company.com"])267 validator.is_valid("alice@company.com") # True268 validator.is_valid("alice@gmail.com") # False269"""270271import re272from typing import Optional273274275def validate_email(email: str, strict: bool = False) -> bool:276 """Validate whether a string is a well-formed email address.277278 Uses a regex pattern to check that the email has a valid local part,279 an @ symbol, a domain, and a TLD of at least 2 characters. Strict mode280 disallows some special characters (e.g., `!`, `#`) in the local part.281282 Args:283 email: The email address string to validate.284 strict: If True, applies stricter character rules in the local part285 (only alphanumeric, dots, underscores, percent, plus, hyphen).286 Defaults to False.287288 Returns:289 True if the email matches the pattern; False otherwise.290291 Examples:292 >>> validate_email("alice@example.com")293 True294 >>> validate_email("not-an-email")295 False296 >>> validate_email("alice!@example.com", strict=True)297 False298 """299 pattern = r'^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$'300 if strict:301 pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'302 return bool(re.match(pattern, email, re.IGNORECASE))303304305def normalize_email(email: str) -> str:306 """Normalize an email address to its canonical form.307308 Strips the plus-addressing suffix from the local part and lowercases309 both the local part and the domain. This enables deduplication of310 addresses like ``Alice+newsletter@Example.COM`` and ``alice@example.com``.311312 Note:313 This function does not validate the email. Call ``validate_email``314 first if you need to ensure the input is well-formed.315316 Args:317 email: A well-formed email address string.318319 Returns:320 The normalized email address in lowercase with plus-addressing removed.321322 Raises:323 ValueError: If the email does not contain an ``@`` symbol.324325 Examples:326 >>> normalize_email("Alice+tag@Example.COM")327 'alice@example.com'328 >>> normalize_email("BOB@COMPANY.ORG")329 'bob@company.org'330 """331 if '@' not in email:332 raise ValueError(f"Invalid email address (missing @): {email!r}")333 local, domain = email.rsplit('@', 1)334 local = local.split('+')[0]335 return f"{local.lower()}@{domain.lower()}"336337338class EmailValidator:339 """Validates email addresses with optional domain allowlisting.340341 Combines format validation with domain restriction. Useful for342 ensuring that only users with addresses from specific domains343 (e.g., corporate email) can register.344345 Attributes:346 allowed_domains: A set of lowercase domain strings that are347 permitted. Empty set means all domains are allowed.348349 Example:350 >>> validator = EmailValidator(allowed_domains=["company.com", "company.org"])351 >>> validator.is_valid("alice@company.com")352 True353 >>> validator.is_valid("bob@gmail.com")354 False355 >>> open_validator = EmailValidator()356 >>> open_validator.is_valid("anyone@anywhere.io")357 True358 """359360 def __init__(self, allowed_domains: Optional[list[str]] = None) -> None:361 """Initialize the validator with an optional domain allowlist.362363 Args:364 allowed_domains: A list of permitted email domains (case-insensitive).365 If None or empty, all domains are allowed.366 """367 self.allowed_domains = {d.lower() for d in (allowed_domains or [])}368369 def is_valid(self, email: str) -> bool:370 """Check whether an email address is valid and from an allowed domain.371372 Args:373 email: The email address to validate.374375 Returns:376 True if the email passes format validation and (if configured)377 its domain is in the allowlist. False otherwise.378 """379 if not validate_email(email):380 return False381 if self.allowed_domains:382 domain = email.rsplit('@', 1)[1].lower()383 return domain in self.allowed_domains384 return True385```386387**JSDoc equivalent (TypeScript):**388```typescript389/**390 * Validates whether a string is a well-formed email address.391 *392 * @param email - The email address string to validate.393 * @param strict - If true, applies stricter character rules in the local part.394 * @returns `true` if the email matches the expected pattern; `false` otherwise.395 *396 * @example397 * ```ts398 * validateEmail("alice@example.com"); // true399 * validateEmail("not-an-email"); // false400 * validateEmail("alice!@example.com", true); // false (strict mode)401 * ```402 */403export function validateEmail(email: string, strict = false): boolean {404 const pattern = strict405 ? /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/i406 : /^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$/i;407 return pattern.test(email);408}409```410411**GoDoc equivalent:**412```go413// Package email provides utilities for validating and normalizing email addresses.414//415// Example usage:416//417// if email.IsValid("alice@example.com") {418// canonical := email.Normalize("Alice+tag@Example.COM")419// fmt.Println(canonical) // alice@example.com420// }421package email422423// IsValid reports whether s is a well-formed email address.424// It uses a regex-based heuristic and is suitable for most real-world425// validation purposes. For strict RFC 5321 compliance, use IsValidStrict.426func IsValid(s string) bool { ... }427428// Normalize returns the canonical form of the email address by lowercasing429// both the local part and domain, and stripping any plus-addressing suffix430// (e.g., "Alice+tag@Example.COM" → "alice@example.com").431//432// Normalize does not validate the email. Call IsValid first if needed.433func Normalize(s string) (string, error) { ... }434```435436## Best Practices437- Write docs before or alongside code, not as an afterthought438- Keep docs close to the code they describe — co-located docstrings beat separate wiki pages439- Treat documentation as code: review it in PRs, update it with behavior changes440- Use examples liberally — a usage example is worth more than three paragraphs of prose441- Make the first sentence of a docstring a complete, standalone description (it appears in IDE tooltips and auto-generated indexes)442443## Common Mistakes444- Restating what the code does ("This function increments x by 1") instead of explaining purpose and contract445- Documenting parameters without types or units (is `timeout` in ms or seconds?)446- Outdated docs that contradict the current behavior — worse than no docs447- Missing exception documentation — callers can't handle errors they don't know about448- Documenting private/internal implementation details that may change vs. the stable public API449450## Tips & Tricks451- `pydoc`, `sphinx`, `typedoc`, and `godoc` generate HTML API references from docstrings automatically452- Keep a Changelog (keepachangelog.com) is the standard CHANGELOG format — follow it consistently453- Use `# TODO(username): reason` format for deferred work so it's trackable454- Add examples to docstrings that are also run as doctests in Python: `pytest --doctest-modules`455- Badge generators: shields.io creates badges for README (CI status, version, license, coverage)456457## Related Skills458- [api-designer](../api-designer/SKILL.md)459- [code-reviewer](../code-reviewer/SKILL.md)460- [refactorer](../refactorer/SKILL.md)461- [test-writer](../test-writer/SKILL.md)