1 / 24

UChicago Data Science Clinic

TA Training

Training Goals

Remember: We're preparing students for real data science careers, not just academic assignments.

Your Responsibilities

Weeks 1-2

Throughout the Quarter

  • Review student pull requests using the /clinic-pr-review Claude skill, then leave the review on the pull request yourself
  • Ensure students are pushing to GitHub, opening pull requests, and opening issues
  • Ensure students are not getting stuck on setup issues and technical bugs
  • Escalate technical issues you cannot solve to clinic staff, and other student concerns to the mentor
  • Post a weekly update on each project to the thread in the clinic TAs Slack channel
  • Run two in-person TA sessions each week and attend the weekly mentor session for each project
  • Take attendance at each TA session and report it weekly in Canvas (5/5 on time, 3/5 up to 10 minutes late, 0 otherwise)
  • Act as the technical advisor on repository structure
  • Keep PRs moving and delete old branches. Reconciling 25 unmerged branches in week 9 is no fun.

Week 10 / Finals

The Weekly Cycle

Student work lives in GitHub. The cycle runs from one mentor session to the next.

Mentor Session
Tasks assigned
Midnight That Day
Issues opened, linked in Slack
Two TA Sessions
Reviews, debugging
Midnight Day Before
Code in PR, status on issues
  • If it isn't on GitHub, we can't see it and it counts as a 0.
  • A task is not complete until the pull request addressing it is merged to main. Incomplete tasks are expected and okay (provided code is pushed).
  • TA sessions are where debugging happens. Mentors are instructed not to debug code during the mentor meeting, so expect that work to be your responsibility.

The Weekly Slack Update

One short post per project:

  • Are any students not pushing code or putting work in issues?
  • Are you caught up with reviews?
  • Are students blocked by anything in particular?
  • Anything to escalate?

Running a TA Session

Checklist

  • Take attendance
  • Review the status of pull requests and reviews
  • For students without active pull requests, review task status and path to completing the task this week
  • Work through technical blockers

 

  • Escalate any technical issue you cannot resolve to clinic staff, and any other student concern to the mentor
  • Check the class schedule and Canvas for upcoming deadlines
  • Enter attendance grades in Canvas

Why These Standards Matter

Reproducibility

Code should work the same way for everyone, everywhere

Professional Skills

Students internalize industry-level practices

Prevent Bugs

Catch "works on my machine" issues early

Fair Reviews

Enable consistent, actionable feedback

Key Technical Areas

  • Code Style & Structure
    • Organization and documentation
  • Debugging & Testing
    • Systematic problem-solving
  • Docker & Make
    • Reproducible environments
  • GitHub Workflows
    • Professional collaboration

For a full list of technical standards and best practices: https://dsi-rse.github.io/ta-training/technical/

Code Style and Structure

Poor Organization

import pandas as pd

df = pd.read_csv("data/survey_2023.csv")
df2 = df[df["q7"] >= 0]
p = len(df2[df2["q7"] >= 9]) / len(df2) * 100
d = len(df2[df2["q7"] <= 6]) / len(df2) * 100
print("2023:", p - d)

df = pd.read_csv("data/survey_2024.csv")
df2 = df[df["q7"] >= 0]
p = len(df2[df2["q7"] >= 9]) / len(df2) * 100
d = len(df2[df2["q7"] <= 6]) / len(df2) * 100
print("2024:", p - d)

df = pd.read_csv("data/survey_2025.csv")
df2 = df[df["q7"] >= 0]
p = len(df2[df2["q7"] >= 9]) / len(df) * 100
d = len(df2[df2["q7"] <= 6]) / len(df) * 100
print("2025:", p - d)

Professional Structure

import pandas as pd

NPS_QUESTION = "q7"   # "How likely are you to recommend us?" (0–10)
PROMOTER_MIN = 9
DETRACTOR_MAX = 6

def net_promoter_score(scores: pd.Series) -> float:
    """% promoters minus % detractors, ignoring negative (missing) codes."""
    valid = scores[scores >= 0]
    promoters = (valid >= PROMOTER_MIN).mean()
    detractors = (valid <= DETRACTOR_MAX).mean()
    return 100 * (promoters - detractors)

def main(years: list[int]) -> None:
    for year in years:
        scores = pd.read_csv(f"data/survey_{year}.csv")[NPS_QUESTION]
        print(f"{year}: {net_promoter_score(scores):+.1f}")

if __name__ == "__main__":
    main([2023, 2024, 2025])

Key Teaching Points: Good structure isn't about following rules - it's about making code maintainable and collaborative. Here the function has a reason to exist (it does a non-trivial thing), the comment is informative (it explains what "q7" is), names are better, computation is separated from I/O, and constants have been defined. Don't break a tiny script into functions that just wrap a pandas call or repeat error-checking pandas already does.

Debugging and Testing

Ad Hoc Debugging

def clean_survey_data(df):
    # Print debugging - can't be controlled
    print("Starting with", len(df), "rows")
    
    # Remove invalid responses
    df = df[df['score'] > 0]
    print("After score filter:", len(df))
    
    # Remove outliers
    df = df[df['score'] < 100]
    print("After outlier removal:", len(df))
    
    return df

# No tests - hope it works!

No Systematic Testing

# Manual testing in notebooks
df = pd.read_csv('data.csv')
result = clean_survey_data(df)
# Visual inspection only
result.head()

Structured Debugging

import logging
logger = logging.getLogger(__name__)

def clean_survey_data(df: pd.DataFrame) -> pd.DataFrame:
    """Clean survey data with logging and validation."""
    logger.info(f"Starting data cleaning with {len(df)} rows")
    
    # Validate input
    required_cols = ['score', 'user_id']
    missing = set(required_cols) - set(df.columns)
    if missing:
        raise ValueError(f"Missing columns: {missing}")
    
    # Remove invalid responses
    initial_count = len(df)
    df_clean = df[df['score'] > 0]
    removed = initial_count - len(df_clean)
    logger.info(f"Removed {removed} rows with invalid scores")
    
    # Remove outliers (configurable threshold)
    outlier_threshold = 100
    df_final = df_clean[df_clean['score'] < outlier_threshold]
    outliers = len(df_clean) - len(df_final)
    logger.info(f"Removed {outliers} outlier rows")
    
    logger.info(f"Cleaning complete: {len(df_final)} rows remaining")
    return df_final

Arrange-Act-Assert Testing

import pandas as pd

# ARRANGE: Set up test data
test_data = pd.DataFrame({
    'user_id': [1, 2, 3, 4],
    'score': [85, -5, 95, 0]
})

# ACT: Call the function
result = clean_survey_data(test_data)

# ASSERT: Verify the expected behavior
assert len(result) == 2, f"Expected 2 rows, got {len(result)}"
assert all(result['score'] > 0), "All scores should be positive"

print("✓ Test passed: Invalid scores removed correctly")


# Test error handling
test_data_bad = pd.DataFrame({'other_col': [1, 2, 3]})

try:
    clean_survey_data(test_data_bad)
    assert False, "Should have raised ValueError"
except ValueError as e:
    assert "Missing columns" in str(e)
    print("✓ Test passed: Missing columns detected")

Key Teaching Points: Systematic debugging saves time and builds professional habits. Look for print statements instead of logging and functions without error handling. The Arrange-Act-Assert pattern structures tests clearly - simple assertions are usually sufficient. While pytest is the professional standard, basic assertions help students start testing without additional tools.

Docker & Make for Reproducibility

Modern Project Structure with uv

# Dockerfile
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim

WORKDIR /app

# Copy dependency files
COPY pyproject.toml uv.lock ./

# Install dependencies using uv
RUN uv sync --frozen --no-dev

# Copy source code
COPY . .

# Default command
CMD ["uv", "run", "python", "app.py"]
# Makefile
IMAGE=project-name
.PHONY: build run test lint sync

build:
	docker build -t $(IMAGE) .

run:
	docker run --rm -it \
		-v $(PWD):/app -w /app \
		$(IMAGE) uv run python app.py

test:
	docker run --rm -it \
		-v $(PWD):/app -w /app \
		$(IMAGE) uv run pytest -q

lint:
	docker run --rm -it \
		-v $(PWD):/app -w /app \
		$(IMAGE) uv run ruff check .

sync:
	docker run --rm -it \
		-v $(PWD):/app -w /app \
		$(IMAGE) uv sync

Key Teaching Points: Modern Python packaging with uv provides faster, more reliable dependency management. Check that projects have pyproject.toml, uv.lock is committed, and make commands use `uv run`. This workflow eliminates "works on my machine" problems.

The Role of AI Coding Agents in Clinic

Expectations

  • AI coding agents are permitted.
  • Students have free Claude licenses and should be using Claude Code as their coding tool, unless there's a compelling reason for an exception.
  • Encourage students to use AI coding agents, especially on monotonous tasks that can be done much faster with AI.
  • Do not expect students to be good at using AI coding agents effectively.

Common Pitfalls

  • Using AI as a crutch: Proceeding through the project with poor understanding. Students should keep a very close hand on their Python code.
  • Accepting output without understanding it: It's dangerous to run commands without having some idea of what they do. Claude is AI and can make mistakes.
  • Letting the agent own version control: Students need to own git as a check on AI.
  • Piling on code: AI is too willing to pile on more code (next slide).
  • AI-written reports: Weekly reports must be written entirely by the student.

How AI Changes Code Quality

Excessively Verbose

def load_and_clean_survey_data(
    file_path: str | Path,
    required_columns: list[str] | None = None,
    drop_duplicates: bool = True,
    verbose: bool = False,
) -> pd.DataFrame | None:
    """Load, validate, and clean survey data robustly."""
    try:
        path = Path(file_path)
        if not path.exists():
            logger.warning(f"File not found: {path}")
            return None
        df = pd.read_csv(path)
    except Exception as e:
        logger.error(f"Failed to load data: {e}")
        return None

    if required_columns is None:
        required_columns = ["user_id", "score"]
    for col in required_columns:
        if col not in df.columns:
            logger.warning(f"Missing column: {col}")

    if drop_duplicates:
        df = df.drop_duplicates()

    try:
        df["score"] = pd.to_numeric(df["score"], errors="coerce")
    except Exception:
        pass

    df = df[df["score"] > 0]
    if verbose:
        print(f"✅ Loaded {len(df)} rows")
    return df

Easy to Read

def load_valid_responses(path: Path) -> pd.DataFrame:
    """Load survey responses with a positive score."""
    df = pd.read_csv(path, usecols=["user_id", "score"])
    return df[df["score"] > 0]

What's Changed

  • AI writes good style at a line-level. But it's too willing to pile on more code.
  • It builds up: Each coding agent session adds more code, and the agent is usually unwilling to make big changes to the previous code
  • Hidden failures: Broad try/except and return None turn clear errors into silent bad data
  • Options no one asked for: Parameters and branches the project never uses

Prompts Students Should Use

  • "Run pre-commit and fix all errors."
  • "Without changing any functionality, simplify it and remove anything unnecessary."

Key Teaching Points: Linters handle line-level style now. Spend your review on size and purpose: Is this the least code necessary? Can the student say why each piece exists? Does it fail loudly when something is wrong? Students must request general clean-ups periodically.

AI-Written Text

PR descriptions, READMEs, issue comments, and notebook markdown are read by humans. Be merciless: expect them to be short, clear, and readable.

AI-Style PR Description

## Summary
This PR introduces a comprehensive, robust, and
scalable data cleaning pipeline that significantly
enhances the overall quality and reliability of
our survey analysis workflow.

## Key Features
- **Robust Data Cleaning**: Implements
  comprehensive validation to ensure...
- **Enhanced Logging**: Provides detailed
  insights into every stage of...
- **Error Handling**: Gracefully handles
  a wide range of edge cases...

## Rough edges worth knowing
- Score coercion is load-bearing here — bad
  values become NaN and quietly drop out.

## Testing
All functionality has been thoroughly tested
to ensure correctness and robustness.

Clear PR Description

Closes #12.

Adds `load_valid_responses()` in `src/survey/load.py`.
It drops rows with non-positive scores (about 4% of
the partner's file).

To check: `make test`, then `make run-pipeline`.
Output is `output/dept_averages.csv`.

Open question: should a score of 0 count as valid?

What to Look For

  • Walls of text: Long, generic prose that says little
  • Dense, jargon-heavy phrasing: "load-bearing," "rough edges worth knowing," em-dashes everywhere (see "Claudish")
  • Headers and bold everywhere: The giant block of markdown gets in the way of the more-important human comments
  • Pasted Claude output: If it must be included, hide it in <details>
  • Notebooks and READMEs: Hard to follow, or describe things the code doesn't do

Key Teaching Points: Hold text to the same standard as code. Request changes on a PR description, README, or notebook just as you would on bad code. Weekly reports must be written entirely by the student.

Claude Code: Are You Set Up?

Claude Code is useful for code reviews; we've provided a /clinic-pr-review skill.

Getting Access

Using /clinic-pr-review

This is our first time using this skill; please provide feedback! You can reach out to discuss on Slack, or you can post issues or contributions to the repo: https://github.com/dsi-rse/skills/.

/clinic-pr-review Checklist: Notebooks

Do they run from a fresh clone, keep functions in modules, and have clear names?

Example /clinic-pr-review checklist with pass, fail, could not verify, and n/a verdicts and evidence for 16 items

/clinic-pr-review Checklist: Code

Does it run in Docker, is it documented, tested, free of dead code, and free of shortcuts?

Example /clinic-pr-review checklist with pass, fail, could not verify, and n/a verdicts and evidence for 16 items

/clinic-pr-review Checklist: Direction

Does the PR link its issue and actually do what the task asked?

Example /clinic-pr-review checklist with pass, fail, could not verify, and n/a verdicts and evidence for 16 items

/clinic-pr-review Checklist: Data Science

Can the results be reproduced, and is the test set kept clean?

Example /clinic-pr-review checklist with pass, fail, could not verify, and n/a verdicts and evidence for 16 items

Practice Time - Let's Review Code Together

def process_survey_data():
    data = pd.read_csv('/Users/alice/Documents/clinic/survey_data.csv')
    
    print("Data loaded successfully!")
    
    # Remove invalid responses
    clean_data = data[data['satisfaction_score'] > 0]
    
    # Calculate statistics  
    avg = clean_data['satisfaction_score'].mean()
    dept_stats = clean_data.groupby('department')['satisfaction_score'].mean()
    
    # Save results
    results = {'overall_avg': avg, 'dept_averages': dept_stats.to_dict()}
    
    with open('analysis_results.json', 'w') as f:
        json.dump(results, f)
    
    print(f"Analysis complete! Average satisfaction: {avg}")
    return results

Discussion Questions

  • What specific issues do you see?
  • How would you prioritize your feedback?
  • What questions would you ask the student?
  • How could this code be improved incrementally?

Git Workflow

main branch: jd/add-validation Squash Merge Initial Other work Feature added Add tests Fix bugs Update docs

Student Workflow

# 1. Create feature branch
git checkout -b jd/my-feature

# 2. Work and commit regularly  
git add .
git commit -m "Add data validation"

# 3. Keep current with main
git checkout main && git pull
git checkout jd/my-feature && git rebase main

# 4. Open PR when ready
# 5. Address feedback
# 6. TA merges after approval

Benefits

  • Clean History: Main shows features, not "fix typo" commits
  • Safe Experimentation: Feature branches can be messy
  • Easy Rollback: Each feature is one commit
  • Parallel Work: Multiple students work simultaneously

Common Issues

  • Working directly on main
  • Not rebasing with main

Key Teaching Points: Feature branches encourage experimentation without fear of breaking main. Squash merging keeps history clean and makes it easy to rollback entire features. Help students understand that messy commits on feature branches are fine - the final merge is what matters.

Reviewing with /clinic-pr-review

Run the Skill
On the student's PR
Verify Findings
Check each one yourself
Write Your Review
Short, specific, human
Follow Up
Did the student respond?

What It Does

  • Runs the code and checks it against clinic standards
  • Installed from dsi-rse/skills (coming soon)
  • Just ask Claude: "Please install the 'clinic-pr-review' skill globally from https://github.com/dsi-rse/skills/"

What It Doesn't Do

  • Replace you. The skill is meant to assist you in reviewing the code, not to replace you.
  • Know what the project actually needs, or whether the student understands their code

Posting the Review

  • Leave the review on the pull request yourself
  • Don't paste the skill's output: a Claude report is usually too detailed, and the giant block of markdown gets in the way of the more-important human comments
  • Pick what matters: A few specific comments beat twenty generated ones
  • If you include Claude output, hide it in <details> … </details>

Use it if and when you find it useful. Sometimes a manual review without Claude assistance may be easier.

Key Teaching Points: Hold your own reviews to the standard you hold students to. If you wouldn't accept a wall of AI text in a student's PR, don't post one in your review.

The Review Feedback Loop

Student Code
Initial implementation
TA Review
Specific, actionable feedback
Student Revision
Address feedback & learn
Final Approval
Standards met

Keys to Effective Feedback

  • Specific: "Add type hints to this function" not "improve code quality"
  • Actionable: Suggest concrete next steps
  • Educational: Explain the "why" behind requests
  • Encouraging: Acknowledge good practices you see

Unhelpful Feedback

  • "This is wrong"
  • "Fix the Docker setup"
  • "Code quality issues"

Helpful Feedback

  • "This function needs type hints - what does it return?"
  • "Can you add a .env.example file showing required variables?"
  • "Consider extracting the file reading into a separate function"

Exception: Vibecoded UIs

If students build a UI for any reason (e.g., a data dashboard), they can take more of a "vibecoding" approach. The code does not need to be reviewed line-for-line, but the PR should say it was vibecoded.

Coaching Techniques

Ask Guiding Questions

  • "What happens if...?"
  • "How would a teammate run this?"
  • "What if this file didn't exist?"

Encourage Small PRs

  • 200-400 lines max
  • AI makes PRs grow fast - push back on size
  • One feature at a time
  • Easier to review and learn from

Model Good Feedback

  • Specific and actionable
  • Explain the "why"
  • Suggest improvements, don't just criticize

Celebrate Progress

  • Acknowledge improvements
  • Progress over perfection
  • Build confidence incrementally

For more code review guidelines and best practices: https://dsi-rse.github.io/ta-training/technical/code-review.html

Common Review Scenarios

What to Look For

  • Hardcoded paths: /Users/alice/project/data.csv
  • Missing error handling: Files that might not exist
  • Unclear function names: process(), calc()
  • Giant files: 500+ lines in one script
  • No type hints: Unclear parameter expectations
  • Secrets in code: API keys, passwords
  • Data in git: GitHub refuses files larger than 100 MB. Consider 10 MB a hard upper limit and 1 MB a soft upper limit. Data belongs in Box.
  • Walls of AI text: Long PR descriptions, READMEs, and notebook markdown

How to Give Feedback

  • Ask questions: "What happens if this file doesn't exist?"
  • Suggest alternatives: "Could we use pathlib.Path here?"
  • Explain benefits: "Type hints help teammates understand your code"
  • Celebrate improvements: "Great job adding docstrings!"

Sample Review Comment

Instead of: "Fix the paths"

Try:

I notice this hardcoded path might break on other machines:

df = pd.read_csv('/Users/alice/project/data.csv')

Could we use a relative path instead? Something like:

from pathlib import Path
data_path = Path(__file__).parent / 'data' / 'survey.csv'
df = pd.read_csv(data_path)

This way anyone can run your code.

Beyond Code Quality

Some of the hardest problems you'll see in a TA session aren't about code at all.

Some Common Issues

Key Teaching Points: Help with these issues when possible, but it can be difficult as a TA to gain a deep understanding of the data and other elements of the content. If students are struggling with these issues, notify the mentor and technical advisor.

When to Escalate

Most projects fail slowly and quietly. Escalate early rather than absorbing the problem yourself.

To Technical Advisor...

Via the mentor-TA Slack channel.

  • A technical problem you cannot solve (environment, Docker, cluster, tooling)
  • A student whose setup still isn't working

If a student can't get their computer set up for a week, that's already ~12% of work time wasted.

To Project Mentor...

The mentor will likely escalate to the clinic director.

  • A student is not pushing code, not attending, or not responding
  • Project-level struggles (problem solving, understanding the data, direction)
  • A student is behaving unprofessionally
  • A student asks you to complete their work
  • A student disputes a grade
  • The external mentor is changing the project scope
# Good escalation message:
"I've noticed 3 teams this week struggling with Docker setup on Windows machines. 
They're getting permission errors when mounting volumes. This is blocking their 
ability to run `make test`. 

Suggested fix: Add Windows-specific Make targets or update the handbook with 
Windows Docker Desktop configuration steps.

This affects ~40% of our current cohort. Can we address this in Friday's 
all-hands meeting?"

Next Steps

Use the Handbook

  • Reference during code reviews
  • Share specific sections with students
  • Bookmark common patterns
  • Suggest handbook updates when needed

Practice First

  • Try the included examples
  • Run through Docker/Make workflows
  • Practice giving constructive feedback
  • Test handbook recommendations

Start Small

  • Focus on 2-3 key areas initially
  • Build consistency gradually
  • Celebrate student improvements
  • Document what works well

Communicate Regularly

  • Weekly Slack update for each project
  • Share challenging scenarios
  • Escalate patterns promptly
  • Contribute to handbook improvements

TA expectations, TA session checklist, and escalation: https://clinic.ds.uchicago.edu/mentor-ta/

Remember: You're Building Professional Data Scientists

Every code review, every standard enforced, every coaching conversation shapes how students will approach data science throughout their careers. The habits they learn here will serve them for years to come.

Questions & Discussion

Thank you!

Let's discuss any questions about:

  • Specific technical standards
  • Code review techniques
  • Student coaching strategies
  • Escalation scenarios
  • Handbook usage and improvements

Full handbook available for reference

Examples in technical/examples/ directory

Additional topics covered in appendix slides

Questions? Reach out anytime!

Appendix

Essential TA Guidelines

Critical topics for effective student coaching

These slides cover essential topics for TAs working with students. Use them as reference during code reviews and student interactions.

Topics covered: Repository hygiene, type hints, Python best practices, Pydantic validation, configuration management, dependency management, I/O separation patterns, and logging & error messages.

Repository Hygiene

Code Debt Management

  • Set the repo structure in week 1: Once branches pile up mid-quarter, reconciliation is expensive
  • Address review comments promptly: Don't let PRs sit for weeks
  • Clean up branches: Delete merged feature branches
  • Regular maintenance: Weekly time for technical debt

Review Timing

  • Pushed every week: Code must be pushed each week, but it doesn't have to be merged that same week
  • Review cycle complete by the following week at the latest
  • Tighter cycles for more changes: The more changes you request, the more important a tight review cycle becomes
  • Maximum 2-3 open PRs per student

Continuous Work Strategy

# While PR is under review, continue building on it
git checkout jd/current-feature
git checkout -b jd/current-feature-part2

# Continue development
git add . && git commit -m "Build on current feature"

# When current PR merges, update and open new PR
git checkout main && git pull
git checkout jd/current-feature-part2
git rebase main

Preventing Bloat

  • Remove unused files: Old notebooks, temp scripts
  • Consolidate code: Merge duplicate functions
  • Clean imports: Remove unused dependencies
  • Archive old work: Move experiments to separate branches

Key Teaching Points: Code debt accumulates quickly over a quarter. Help students develop habits of regular maintenance and prompt response to feedback. A clean repository is easier to review, understand, and maintain.

Type Hints for Clarity

Unclear Function

def process_data(data, config):
    # What type is data? 
    # What does config contain?
    # What gets returned?
    result = analyze(data, config)
    return result

Runtime Error

def calculate_average(scores):
    return sum(scores) / len(scores)

# Later...
calculate_average("85,90,78")  # TypeError!

Clear Expectations

def process_survey_data(
    data: pd.DataFrame, 
    config: AnalysisConfig
) -> dict[str, float]:
    """Process survey data and return statistics."""
    result = analyze(data, config)
    return result

Early Detection

def calculate_average(scores: list[float]) -> float:
    """Calculate mean of numeric scores."""
    return sum(scores) / len(scores)

# Type checker catches error before runtime
calculate_average("85,90,78")  # mypy error!

Key Teaching Points: Type hints aren't just documentation - they're early error detection. Focus on public functions and anywhere data types aren't obvious. Help students see how type hints make code self-documenting and catch bugs before runtime.

Python Best Practices

Mutable Default Arguments

def add_item(item, items=[]):
    items.append(item)
    return items

# Dangerous! Same list shared across calls
list1 = add_item("apple")     # ["apple"]
list2 = add_item("banana")    # ["apple", "banana"]

Variable Scope Issues

results = []
for i in range(3):
    # Late binding closure problem
    results.append(lambda: i * 2)

# All functions return 4 (i=2)
[f() for f in results]  # [4, 4, 4]

Safe Default Arguments

def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

# Each call gets fresh list
list1 = add_item("apple")     # ["apple"]
list2 = add_item("banana")    # ["banana"]

Explicit Closures

results = []
for i in range(3):
    # Capture i explicitly
    results.append(lambda x=i: x * 2)

# Each function has its own value
[f() for f in results]  # [0, 2, 4]

Key Teaching Points: These aren't style issues - they're bugs waiting to happen. Help students understand why Python behaves this way.

Data Validation with Pydantic

Without Validation

def process_config(config_dict):
    # What if keys are missing?
    # What if values are wrong type?
    db_url = config_dict["database_url"]
    max_workers = config_dict["max_workers"]
    debug = config_dict["debug_mode"]
    
    # Runtime errors waiting to happen
    return setup_pipeline(db_url, max_workers, debug)

With Pydantic

from pydantic import BaseModel, Field, field_validator

class PipelineConfig(BaseModel):
    database_url: str = Field(..., min_length=1)
    max_workers: int = Field(default=2, ge=1, le=16)
    debug_mode: bool = Field(default=False)
    
    @field_validator('database_url')
    @classmethod
    def validate_db_url(cls, v: str) -> str:
        if not v.startswith(('postgresql://', 'sqlite://')):
            raise ValueError('Invalid database URL')
        return v

# Validates automatically on creation
config = PipelineConfig(**config_dict)

Key Teaching Points: Pydantic isn't just validation - it's early error detection that saves debugging time. When students load configuration or API data, suggest Pydantic to catch problems before they become mysterious runtime failures.

Configuration Management

Hardcoded Configuration

def analyze_data():
    # Hardcoded values scattered throughout
    df = pd.read_csv("/Users/alice/data/survey.csv")
    
    # Magic numbers with no context
    threshold = 0.95
    min_samples = 50
    
    # Database connection hardcoded
    conn = sqlite3.connect("project_db.sqlite")
    
    # API key in source code
    api_key = "sk-1234567890abcdef"

Environment Variables

# .env file (often missing .env.example)
DATA_PATH=/Users/alice/data/survey.csv
CONFIDENCE_LEVEL=0.95
MIN_SAMPLES=50
DB_URL=sqlite:///project_db.sqlite
API_KEY=sk-1234567890abcdef

Pydantic Settings Class

from pathlib import Path
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

class AnalysisConfig(BaseSettings):
    """Centralized configuration with validation."""
    
    model_config = SettingsConfigDict(env_file=".env")
    
    # Data paths with validation
    data_path: Path = Field(..., description="Path to survey data")
    output_dir: Path = Field(default=Path("./outputs"))
    
    # Analysis parameters with constraints
    confidence_level: float = Field(default=0.95, ge=0.5, le=0.99)
    min_samples: int = Field(default=50, ge=1)
    
    # External services
    database_url: str = Field(..., description="Database connection string")
    api_key: str = Field(..., description="External API key")
    
    @field_validator('data_path')
    @classmethod
    def validate_data_path(cls, v: Path) -> Path:
        if not v.exists():
            raise ValueError(f"Data file not found: {v}")
        return v

# Usage with automatic validation
config = AnalysisConfig()

.env.example File

# .env.example - commit this file
DATA_PATH=./data/survey.csv
CONFIDENCE_LEVEL=0.95
MIN_SAMPLES=50
DATABASE_URL=sqlite:///your_database.sqlite
API_KEY=your_api_key_here

Key Teaching Points: Configuration management prevents "works on my machine" problems. Look for hardcoded values, missing .env.example files, and secrets in code. Pydantic settings classes catch configuration errors early and document what's required.

Dependencies and Environments

Unpinned Dependencies

# Old requirements.txt approach
pandas
numpy
scikit-learn
matplotlib
requests

Local Installation

# Student runs on their laptop
pip install pandas numpy scikit-learn

# Works for them, breaks for teammates
# Different package versions
# Different Python versions
# Different operating systems

Missing Documentation

# README.md
## Setup
1. Install Python
2. Run the code
3. Hope it works

Modern Dependencies

# pyproject.toml
[project]
name = "survey-analysis"
dependencies = [
    "pandas==2.2.2",
    "numpy==1.26.4",
    "scikit-learn==1.4.2",
]

[project.optional-dependencies]
dev = [
    "pytest==8.1.1",
    "ruff==0.4.2",
]

uv + Docker Environment

# Dockerfile
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim

WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
COPY . .
CMD ["uv", "run", "python", "app.py"]

Clear Setup Instructions

# README.md
## Setup
1. Install Docker Desktop
2. Clone this repository
3. Run: `make build`
4. Run: `make test`
5. Start analysis: `make run`

## Development
- `make shell` - Interactive container
- `make sync` - Update dependencies

Key Teaching Points: Modern dependency management with uv and lock files prevents collaboration breakdowns. Check for missing pyproject.toml, unpinned versions, and missing uv.lock files. Docker + uv isn't complexity - it's consistency across team members.

Separate I/O from Computation

Mixed Together

def analyze_survey():
    # I/O mixed with computation
    df = pd.read_csv("data/survey.csv")
    
    # Analysis logic
    avg_score = df['satisfaction'].mean()
    
    # More I/O
    with open("results.json", "w") as f:
        json.dump({"avg": avg_score}, f)
    
    print(f"Average: {avg_score}")

Separated

def calculate_satisfaction_stats(
    df: pd.DataFrame
) -> dict[str, float]:
    """Pure function - easy to test."""
    return {
        'avg_score': df['satisfaction'].mean(),
        'std_score': df['satisfaction'].std()
    }

def main():
    # I/O layer
    df = pd.read_csv(DATA_PATH)
    
    # Pure computation  
    stats = calculate_satisfaction_stats(df)
    
    # I/O layer
    save_results(stats, OUTPUT_PATH)

Key Teaching Points: I/O separation makes code testable and reusable. Look for functions that both read files AND do computation - that's a refactoring opportunity. Pure functions are easier to test, debug, and understand.

Logging & Specific Error Messages

Poor Error Handling

def load_data(path):
    df = pd.read_csv(path)  
    # FileNotFoundError: [Errno 2] No such file
    
    assert 'score' in df.columns  
    # AssertionError (no context!)
    
    print("Data loaded")  # Can't control output
    return df

Helpful Error Messages

import logging
logger = logging.getLogger(__name__)

def load_survey_data(path: Path) -> pd.DataFrame:
    if not path.exists():
        raise FileNotFoundError(
            f"Survey file not found: {path}\n"
            f"Current directory: {Path.cwd()}\n"  
            f"Expected location: {path.absolute()}"
        )
    
    df = pd.read_csv(path)
    
    required_cols = ['user_id', 'satisfaction']
    missing = set(required_cols) - set(df.columns)
    if missing:
        raise ValueError(
            f"Missing columns in {path.name}: {missing}\n"
            f"Available: {list(df.columns)}"
        )
    
    logger.info(f"Loaded {len(df)} survey responses")
    return df

Key Teaching Points: Good error messages are love letters to future debuggers. Replace generic exceptions with specific, actionable messages. Logging beats print statements - it can be controlled and filtered appropriately.