529 vs taxable brokerage after-tax tradeoff engine
Project description
plan529lab
A Python package for evaluating the after-tax tradeoff between investing through a 529/Qualified Tuition Program (QTP) and a taxable brokerage account for education savings.
This is not a simple "529 penalty calculator." It is a scenario engine that compares after-tax outcomes across multiple future states — including qualified education use, nonqualified withdrawal, beneficiary change, and Roth IRA rollover paths.
Disclaimer: This package is for educational and analytical purposes only. It is not tax, legal, or investment advice. Tax outcomes depend on facts, jurisdiction, and future law changes. Consult a qualified tax professional for your situation.
Why This Exists
Public discussion of 529 plans often oversimplifies the tradeoff. Common claims that 529s are "risky" because of "penalties" miss key nuances:
- Only the earnings portion of a nonqualified withdrawal is taxable — contributions/basis come back tax-free
- The 10% additional tax applies to the amount included in income, not the entire withdrawal
- Qualified expenses must be reduced by tax-free educational assistance and expenses used for AOTC/LLC
- State tax benefits, recapture rules, and the Roth rollover path materially affect the comparison
This package makes these interactions explicit and quantifiable.
Installation
pip install plan529lab
For development:
pip install -e ".[dev]"
Requires Python 3.11+.
Quick Start
Python API
from plan529lab.api import analyze_tradeoff
from plan529lab.io.yaml_loader import load_config
from plan529lab.state_rules.no_income_tax import NoIncomeTaxStateRule
config = load_config("examples/washington_no_income_tax.yaml")
result = analyze_tradeoff(config, state_rule=NoIncomeTaxStateRule("WA"))
print(result.explain())
print(f"Delta: ${result.delta:,.2f}")
print(f"Break-even probability: {result.break_even_qualified_use_probability:.1%}")
Monte Carlo Simulation
from plan529lab.api import run_monte_carlo
from plan529lab.models.monte_carlo import MonteCarloConfig, StochasticAssumptions
mc_config = MonteCarloConfig(
n_paths=10_000,
seed=42,
stochastic=StochasticAssumptions(return_std=0.15),
)
mc_result = run_monte_carlo(config, mc_config)
print(f"P(529 wins): {mc_result.prob_qtp_wins:.1%}")
print(f"Mean delta: ${mc_result.mean_delta:,.2f}")
Sensitivity Analysis
from plan529lab.api import run_sensitivity
result = run_sensitivity(config, "qualified_use_probability", [0.0, 0.25, 0.5, 0.75, 1.0])
for v, d in zip(result.param_values, result.deltas):
print(f" p={v:.0%}: delta=${d:,.0f}")
CLI
# Deterministic analysis
python -m plan529lab analyze --config examples/washington_no_income_tax.yaml
# Monte Carlo simulation
python -m plan529lab monte-carlo --config examples/washington_no_income_tax.yaml --n-sims 10000 --seed 42
# Sensitivity analysis
python -m plan529lab sensitivity --config examples/washington_no_income_tax.yaml \
--param qualified_use_probability --min 0 --max 1 --steps 11
# Break-even probability
python -m plan529lab breakeven --config examples/washington_no_income_tax.yaml
# State rule info
python -m plan529lab state-info WA
Configuration
Scenarios are defined in YAML files. See examples/ for templates.
tax_profile:
ordinary_income_rate: 0.35
ltcg_rate: 0.15
qualified_dividend_rate: 0.15
portfolio_assumptions:
annual_return: 0.07
dividend_yield: 0.015
qualified_dividend_share: 0.95
turnover_realization_rate: 0.05
scenario_policy:
qualified_use_probability: 0.75
horizon_years: 18
State Rules
The package uses a plugin architecture for state-specific tax treatment:
- NoIncomeTaxStateRule — for WA, TX, FL, NV, SD, WY, AK, TN, NH
- GenericDeductionStateRule — models a state income tax deduction on 529 contributions
- GenericCreditStateRule — models a state tax credit on 529 contributions
Generic rules do not reflect any specific state's exact rules, caps, or conditions.
Assumptions and Limitations
- Annual timesteps with start-of-year contributions
- Simplified taxable account model (no lot-level accounting, wash-sale rules, or AMT)
- Tax rates are assumed constant over the horizon
- Roth rollover uses one-shot eligibility estimate (not year-by-year staged rollover)
- Monte Carlo treats dividend yield and turnover as per-path constants (not per-year)
- State rules are generic unless specifically implemented
Development
# Install dev dependencies
pip install -e ".[dev]"
# Run tests
pytest -v
# Lint
ruff check .
# Type check
mypy .
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file plan529lab-0.1.0.tar.gz.
File metadata
- Download URL: plan529lab-0.1.0.tar.gz
- Upload date:
- Size: 58.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eb86a6023e9022e99e53c27a14c70c73dee56e36da57bfef32c371badc319dec
|
|
| MD5 |
ea0c447f9738668736419a80558a01f0
|
|
| BLAKE2b-256 |
b1df9cd6f3a209b9240634886f85f440e900a03bedd52db78ae8eb2f15495b40
|
Provenance
The following attestation bundles were made for plan529lab-0.1.0.tar.gz:
Publisher:
publish.yml on engineerinvestor/plan529lab
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
plan529lab-0.1.0.tar.gz -
Subject digest:
eb86a6023e9022e99e53c27a14c70c73dee56e36da57bfef32c371badc319dec - Sigstore transparency entry: 1210585305
- Sigstore integration time:
-
Permalink:
engineerinvestor/plan529lab@52925899029ea8573be20005600334b279c6ffae -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/engineerinvestor
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@52925899029ea8573be20005600334b279c6ffae -
Trigger Event:
release
-
Statement type:
File details
Details for the file plan529lab-0.1.0-py3-none-any.whl.
File metadata
- Download URL: plan529lab-0.1.0-py3-none-any.whl
- Upload date:
- Size: 40.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
49e4949755b978029d716d968237e9d81be8a6982e844bae7c39a93cc5185d18
|
|
| MD5 |
3b7a12527083e6e1c640fcfaf46770fc
|
|
| BLAKE2b-256 |
954ece40f2843189d5b471ebb5d9dfc8b1057afe60a54cf6f5fb8975b7f33b5a
|
Provenance
The following attestation bundles were made for plan529lab-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on engineerinvestor/plan529lab
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
plan529lab-0.1.0-py3-none-any.whl -
Subject digest:
49e4949755b978029d716d968237e9d81be8a6982e844bae7c39a93cc5185d18 - Sigstore transparency entry: 1210585332
- Sigstore integration time:
-
Permalink:
engineerinvestor/plan529lab@52925899029ea8573be20005600334b279c6ffae -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/engineerinvestor
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@52925899029ea8573be20005600334b279c6ffae -
Trigger Event:
release
-
Statement type: