# Rendering and Writing Files in Python

With the CLI shell in place, the next problem was what `scaffoldr init` actually creates. In v0.1.0, every project got the same directory structure and files - README, CONTRIBUTING guide, `pyproject.toml`, an ADR template, a .gitignore, and a CI workflow. All hardcoded.

## String templates as functions

Each file's content lives in its own function, returning a formatted string. Here's the README:

````python
def readme(project_name: str, author: str) -> str:
    return f"""\
# {project_name}
> Short description of what this project does.
## Installation
```bash
pip install {project_name}
```
## Usage
```bash
{project_name} --help
```
## Development
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and
contribution guidelines.
## License
{author} - MIT
"""
````

The `\` right after the opening `"""` strips the leading newline that would otherwise appear before `# {project_name}`. Without it, the string starts with a blank line.

Every file in the scaffold follows this pattern - a function that takes whatever variables it needs (`project_name`, `author`, `python_version`) and returns the finished file content as a f-string.

## Why functions instead of one big template

I could have written this as one function - something that takes every possible variable (`project_name`, `author`, `python_version`, `license_`) plus a parameter saying which file's content to return - then branching internally to build the right string.

Splitting it into six separate functions instead means each one's signature only lists what it actually uses. `gitignore()` takes nothing. `pyproject()` takes `project_name`, `author`, `license_`, `python_version`. A single dispatcher function would need every one of those parameters available at once, even though most individual files only use a subset.

It also means adding a new content-generating function later doesn't touch the existing ones - no shared dispatcher to extend, no risk of breaking `readme()` while adding `license_file()`.

## Creating the folder structure

The actual scaffolding happens in `scaffold`:

```python
def scaffold(project_name: str, path: Path) -> None:
    cfg = Config.load()
    root = path / project_name
    if root.exists():
        typer.echo(f"Error: {root} already exists.", err=True)
        raise typer.Exit(code=1)
    typer.echo(f"Creating project at {root} ...")
    (root / project_name).mkdir(parents=True)
    (root / "tests").mkdir()
    (root / "docs" / "adr").mkdir(parents=True)
    (root / ".github" / "workflows").mkdir(parents=True)
```

Before creating anything, `scaffold` checks if root already exists and exits with an error if it does. This avoids silently overwriting someone's files.

`mkdir(parents=True)` creates every missing directory in the path, not just the final one. `docs / adr` is two levels deep - `docs` and `docs/adr`. Without `parents=True`, you'd need two separate `mkdir()` calls, one for each level.

## Writing the files

Once the directories exist, writing files is straightforward:

```python
(root / "README.md").write_text(readme(project_name, cfg.author))
(root / "CONTRIBUTING.md").write_text(contributing(project_name))
(root / "pyproject.toml").write_text(
    pyproject(
        project_name, cfg.author, cfg.python_version, cfg.license
    )
)
```

Each file gets written the same way - call the content-generating function, pass the result to `Path.write_text()`.

`__init__.py` files follow the same pattern, just with empty strings:

```python
(root / "tests" / "__init__.py").write_text("")
(root / project_name / "__init__.py").write_text("")
```

This makes both the project's own package folder and its test folder proper Python packages.

## What's next

At this point, scaffold finishes by initializing git and making the first commit. That's the next post.

The code is on [GitHub](https://github.com/mg4603/scaffoldr).
