Hey I'm a physiotherapy student in 2nd year in my class my faculty teaches us how to write assessment of patient but I was not present can u tell me how to write it step by step and how to know which problem is patient problem
physiotherapy patient assessment step by step subjective objective analysis plan SOAP note for students
Example: "I have pain in my right knee when I climb stairs."
| Letter | Meaning | What to ask |
|---|---|---|
| O | Onset | When did it start? Sudden or gradual? |
| P | Provocation/Palliation | What makes it worse? What relieves it? |
| Q | Quality | Describe the pain - sharp, dull, burning, aching? |
| R | Radiation | Does the pain travel anywhere? |
| S | Severity | Rate pain on a scale of 0-10 (VAS or NRS) |
| T | Timing | Constant or intermittent? Morning or evening? |
Example: reduced ROM, muscle weakness grade 3, positive SLR, pain 7/10, unable to walk >10 minutes
Example: "Patient presents with right-sided lumbar radiculopathy at L4-L5 level with significant functional limitation in walking and ADL, likely secondary to disc herniation."
| Problem | Intervention |
|---|---|
| Pain | TENS, ice/heat, US therapy, joint mobilization |
| Reduced ROM | Passive stretching, joint mobilization, PNF |
| Weakness | Strengthening exercises (open/closed chain) |
| Gait deviation | Gait training, walking aids if needed |
| Functional limitation | Functional retraining, stair training, ADL practice |
TALK to patient (S) → EXAMINE patient (O) → THINK & IDENTIFY PROBLEMS (A) → TREAT problems (P)
Create a study guide document with SOAP note templates and checklists
ls -la /tmp/workspace/ 2>/dev/null || echo "workspace empty or missing"
mkdir -p /tmp/workspace/physio-study-guide
~/physio-study-guide/build_guide.py
from docx import Document
from docx.shared import Pt, Inches, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
import copy
OUTPUT = "/tmp/workspace/physio-study-guide/Physiotherapy_SOAP_Study_Guide.docx"
doc = Document()
# ── Page margins ────────────────────────────────────────────────────────────
section = doc.sections[0]
section.top_margin = Inches(1.0)
section.bottom_margin = Inches(1.0)
section.left_margin = Inches(1.1)
section.right_margin = Inches(1.1)
# ── Default Normal style ─────────────────────────────────────────────────────
normal = doc.styles["Normal"]
normal.font.name = "Calibri"
normal.font.size = Pt(11)
# ── Heading styles ───────────────────────────────────────────────────────────
def style_heading(doc, level, size, bold, color_hex):
style = doc.styles[f"Heading {level}"]
style.font.name = "Calibri"
style.font.size = Pt(size)
style.font.bold = bold
r, g, b = int(color_hex[0:2],16), int(color_hex[2:4],16), int(color_hex[4:6],16)
style.font.color.rgb = RGBColor(r, g, b)
style.paragraph_format.space_before = Pt(10)
style.paragraph_format.space_after = Pt(4)
style_heading(doc, 1, 20, True, "1A4F8A") # dark blue
style_heading(doc, 2, 14, True, "2E75B6") # medium blue
style_heading(doc, 3, 12, True, "2E86AB") # teal-blue
style_heading(doc, 4, 11, True, "404040") # dark grey
# ── Helpers ──────────────────────────────────────────────────────────────────
def add_paragraph(text, bold=False, italic=False, size=11, color_hex=None, indent=0, space_after=4):
p = doc.add_paragraph()
p.paragraph_format.space_after = Pt(space_after)
p.paragraph_format.space_before = Pt(2)
if indent:
p.paragraph_format.left_indent = Inches(indent)
run = p.add_run(text)
run.bold = bold
run.italic = italic
run.font.size = Pt(size)
if color_hex:
r,g,b = int(color_hex[0:2],16),int(color_hex[2:4],16),int(color_hex[4:6],16)
run.font.color.rgb = RGBColor(r,g,b)
return p
def add_bullet(text, bold_prefix=None, indent_level=0, size=11):
"""Add a bullet point; bold_prefix is bolded, rest is normal."""
p = doc.add_paragraph(style="List Bullet")
p.paragraph_format.space_after = Pt(2)
p.paragraph_format.space_before = Pt(1)
p.paragraph_format.left_indent = Inches(0.25 + indent_level * 0.25)
if bold_prefix:
run = p.add_run(bold_prefix)
run.bold = True
run.font.size = Pt(size)
run2 = p.add_run(text)
run2.font.size = Pt(size)
else:
run = p.add_run(text)
run.font.size = Pt(size)
def add_checkbox(text, bold_prefix=None, size=11):
"""Add a checkbox bullet (□)."""
p = doc.add_paragraph()
p.paragraph_format.space_after = Pt(2)
p.paragraph_format.space_before = Pt(1)
p.paragraph_format.left_indent = Inches(0.25)
run = p.add_run("☐ ")
run.font.size = Pt(size)
run.font.color.rgb = RGBColor(0x2E, 0x75, 0xB6)
if bold_prefix:
rb = p.add_run(bold_prefix)
rb.bold = True
rb.font.size = Pt(size)
r2 = p.add_run(text)
r2.font.size = Pt(size)
else:
r = p.add_run(text)
r.font.size = Pt(size)
def add_shaded_box(text, shade_hex="E8F0F9", text_color="1A4F8A", bold=True, size=11):
"""Add a paragraph with background shading (box effect)."""
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(6)
p.paragraph_format.space_after = Pt(6)
p.paragraph_format.left_indent = Inches(0.1)
# shading via XML
pPr = p._p.get_or_add_pPr()
shd = OxmlElement("w:shd")
shd.set(qn("w:val"), "clear")
shd.set(qn("w:color"), "auto")
shd.set(qn("w:fill"), shade_hex)
pPr.append(shd)
run = p.add_run(text)
run.bold = bold
run.font.size = Pt(size)
r,g,b = int(text_color[0:2],16),int(text_color[2:4],16),int(text_color[4:6],16)
run.font.color.rgb = RGBColor(r,g,b)
return p
def add_table_header_row(table, headers, header_color="1A4F8A"):
"""Style the first row as a header."""
row = table.rows[0]
for i, hdr in enumerate(headers):
cell = row.cells[i]
cell.text = hdr
# background
tc_pr = cell._tc.get_or_add_tcPr()
shd = OxmlElement("w:shd")
shd.set(qn("w:val"), "clear")
shd.set(qn("w:color"), "auto")
shd.set(qn("w:fill"), header_color)
tc_pr.append(shd)
para = cell.paragraphs[0]
run = para.runs[0] if para.runs else para.add_run(hdr)
run.bold = True
run.font.color.rgb = RGBColor(255,255,255)
run.font.size = Pt(10)
para.paragraph_format.space_after = Pt(2)
para.paragraph_format.space_before = Pt(2)
def set_cell_text(cell, text, bold=False, size=10, shade=None):
cell.text = ""
para = cell.paragraphs[0]
para.paragraph_format.space_after = Pt(2)
para.paragraph_format.space_before = Pt(2)
run = para.add_run(text)
run.bold = bold
run.font.size = Pt(size)
if shade:
tc_pr = cell._tc.get_or_add_tcPr()
shd = OxmlElement("w:shd")
shd.set(qn("w:val"), "clear")
shd.set(qn("w:color"), "auto")
shd.set(qn("w:fill"), shade)
tc_pr.append(shd)
def hr():
"""Add a thin horizontal rule."""
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(4)
p.paragraph_format.space_after = Pt(4)
pPr = p._p.get_or_add_pPr()
pBdr = OxmlElement("w:pBdr")
bottom = OxmlElement("w:bottom")
bottom.set(qn("w:val"), "single")
bottom.set(qn("w:sz"), "6")
bottom.set(qn("w:space"),"1")
bottom.set(qn("w:color"),"2E75B6")
pBdr.append(bottom)
pPr.append(pBdr)
def page_break():
doc.add_page_break()
# ════════════════════════════════════════════════════════════════════════════
# COVER / TITLE PAGE
# ════════════════════════════════════════════════════════════════════════════
# Big title
p_title = doc.add_paragraph()
p_title.alignment = WD_ALIGN_PARAGRAPH.CENTER
p_title.paragraph_format.space_before = Pt(40)
p_title.paragraph_format.space_after = Pt(6)
rt = p_title.add_run("Physiotherapy Patient Assessment")
rt.bold = True
rt.font.size = Pt(26)
rt.font.color.rgb = RGBColor(0x1A, 0x4F, 0x8A)
p_sub = doc.add_paragraph()
p_sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
p_sub.paragraph_format.space_after = Pt(4)
rs = p_sub.add_run("SOAP Note Study Guide & Templates")
rs.bold = True
rs.font.size = Pt(18)
rs.font.color.rgb = RGBColor(0x2E, 0x75, 0xB6)
p_tag = doc.add_paragraph()
p_tag.alignment = WD_ALIGN_PARAGRAPH.CENTER
p_tag.paragraph_format.space_after = Pt(50)
rl = p_tag.add_run("For 2nd Year Physiotherapy Students | Compiled for Clinical Practice")
rl.italic = True
rl.font.size = Pt(11)
rl.font.color.rgb = RGBColor(0x60,0x60,0x60)
# decorative shaded tagline box
tb = doc.add_paragraph()
tb.alignment = WD_ALIGN_PARAGRAPH.CENTER
tb.paragraph_format.space_before = Pt(10)
tb.paragraph_format.space_after = Pt(10)
pPr = tb._p.get_or_add_pPr()
shd = OxmlElement("w:shd")
shd.set(qn("w:val"), "clear"); shd.set(qn("w:color"),"auto"); shd.set(qn("w:fill"),"1A4F8A")
pPr.append(shd)
rr = tb.add_run(" S • O • A • P ")
rr.bold = True
rr.font.size = Pt(28)
rr.font.color.rgb = RGBColor(255,255,255)
p_exp = doc.add_paragraph()
p_exp.alignment = WD_ALIGN_PARAGRAPH.CENTER
p_exp.paragraph_format.space_before = Pt(4)
rexp = p_exp.add_run("Subjective • Objective • Assessment • Plan")
rexp.font.size = Pt(12)
rexp.font.color.rgb = RGBColor(0x2E,0x75,0xB6)
page_break()
# ════════════════════════════════════════════════════════════════════════════
# TABLE OF CONTENTS (manual)
# ════════════════════════════════════════════════════════════════════════════
doc.add_heading("Table of Contents", level=1)
toc_items = [
("Section 1", "SOAP Note — What It Is & Why We Use It"),
("Section 2", "Step-by-Step: S — Subjective Assessment"),
("Section 3", "Step-by-Step: O — Objective Assessment"),
("Section 4", "Step-by-Step: A — Assessment & Problem Identification"),
("Section 5", "Step-by-Step: P — Plan"),
("Section 6", "How to Identify the Patient's Problem (ICF Model)"),
("Section 7", "OPQRST Pain Assessment Cheat Sheet"),
("Section 8", "MRC Muscle Grading Scale"),
("Section 9", "SOAP Note Blank Template (Musculoskeletal)"),
("Section 10", "SOAP Note Blank Template (Neurological)"),
("Section 11", "Pre-Assessment Checklist"),
("Section 12", "Worked Example — Knee Pain Patient"),
("Section 13", "Common Mistakes to Avoid"),
]
for sec, title in toc_items:
p = doc.add_paragraph()
p.paragraph_format.space_after = Pt(3)
p.paragraph_format.space_before = Pt(1)
r1 = p.add_run(f"{sec}: ")
r1.bold = True
r1.font.size = Pt(11)
r1.font.color.rgb = RGBColor(0x1A,0x4F,0x8A)
r2 = p.add_run(title)
r2.font.size = Pt(11)
page_break()
# ════════════════════════════════════════════════════════════════════════════
# SECTION 1 — What is a SOAP Note
# ════════════════════════════════════════════════════════════════════════════
doc.add_heading("Section 1: SOAP Note — What It Is & Why We Use It", level=1)
add_paragraph(
"A SOAP note is the standard documentation format used in physiotherapy (and all allied health professions). "
"It was developed by Dr. Lawrence Weed as part of the Problem-Oriented Medical Record (POMR) system. "
"It organises all clinical information about a patient in a logical, reproducible way.",
size=11
)
add_paragraph(
"SOAP stands for:",
bold=True, size=11
)
# Summary table
tbl = doc.add_table(rows=5, cols=3)
tbl.style = "Table Grid"
tbl.autofit = False
tbl.columns[0].width = Inches(1.0)
tbl.columns[1].width = Inches(1.5)
tbl.columns[2].width = Inches(3.5)
headers = ["Letter", "Full Word", "What It Covers"]
add_table_header_row(tbl, headers)
rows_data = [
("S", "Subjective", "What the patient tells you — pain, history, complaints, goals"),
("O", "Objective", "What you measure and observe — ROM, strength, special tests"),
("A", "Assessment", "Your clinical interpretation — problems, diagnosis, priorities"),
("P", "Plan", "What you will do — treatment goals, interventions, frequency"),
]
for i, (l, w, c) in enumerate(rows_data, start=1):
shade = "E8F4FD" if i % 2 == 0 else None
set_cell_text(tbl.rows[i].cells[0], l, bold=True, size=10, shade=shade)
set_cell_text(tbl.rows[i].cells[1], w, bold=True, size=10, shade=shade)
set_cell_text(tbl.rows[i].cells[2], c, size=10, shade=shade)
doc.add_paragraph()
add_shaded_box(
"Key Principle: Each section must be kept SEPARATE. Never mix subjective data into the objective section "
"or write your interpretation in the objective section.",
shade_hex="FFF3CD", text_color="7B4F00"
)
page_break()
# ════════════════════════════════════════════════════════════════════════════
# SECTION 2 — Subjective
# ════════════════════════════════════════════════════════════════════════════
doc.add_heading("Section 2: Step-by-Step — S (Subjective Assessment)", level=1)
add_shaded_box("This is information from the PATIENT — you cannot measure it. Record it, do not interpret it yet.")
doc.add_heading("2.1 Patient Demographics", level=2)
for item in [
"Name, Age, Sex",
"Occupation (important — affects load, posture, repetitive movements)",
"Dominant hand / dominant leg",
"Date of assessment",
"Referring doctor / referral diagnosis (if any)",
]:
add_checkbox(item)
doc.add_heading("2.2 Chief Complaint (CC)", level=2)
add_paragraph("Write in the patient's own words, in quotation marks:", size=11)
add_paragraph('Example: "I have pain in my right knee when I climb stairs."', italic=True, indent=0.3, size=11)
add_paragraph(
"Note: location, nature of complaint, and what activity brings it on. This is the patient's NUMBER ONE problem in their own words.",
size=10, color_hex="555555"
)
doc.add_heading("2.3 History of Present Illness (HPI) — Use OPQRST", level=2)
add_paragraph("Ask each OPQRST question systematically (full cheat sheet in Section 7):", size=11)
for item in [
("O — Onset: ", "When did it start? Was it sudden (trauma) or gradual (overuse)?"),
("P — Provocation/Palliation: ", "What makes it WORSE? What makes it BETTER?"),
("Q — Quality: ", "Describe the pain — sharp, dull, burning, aching, throbbing?"),
("R — Radiation: ", "Does the pain travel anywhere (nerve involvement)?"),
("S — Severity: ", "Rate pain 0-10. Also rate at rest vs. activity."),
("T — Timing: ", "Constant or intermittent? Morning stiffness? Night pain?"),
]:
add_bullet(item[1], bold_prefix=item[0])
doc.add_heading("2.4 Past Medical & Surgical History", level=2)
for item in [
"Previous injuries to same area",
"Previous surgeries (note dates)",
"Hospitalizations relevant to current complaint",
"Chronic conditions (diabetes, hypertension, osteoporosis, arthritis)",
]:
add_checkbox(item)
doc.add_heading("2.5 Drug History", level=2)
add_paragraph("Especially note:", size=11)
for item in [
"NSAIDs / analgesics (tells you pain is being managed — may mask symptoms)",
"Steroids (affects healing, bone density)",
"Blood thinners / anticoagulants (affects manual therapy decisions)",
"Muscle relaxants / neurological drugs",
]:
add_bullet(item)
doc.add_heading("2.6 Social & Occupational History", level=2)
for item in [
"Type of job (desk job / manual labor / standing all day)",
"Home situation (stairs? caregiver support?)",
"Hobbies and sports",
"Smoking / alcohol (affects healing and circulation)",
]:
add_checkbox(item)
doc.add_heading("2.7 Patient's Goals", level=2)
add_paragraph(
"Always ask: 'What do you want to be able to do that you cannot do now?' "
"This sets the direction for your plan and helps measure success.",
size=11
)
add_bullet("Short-term goal (1-2 weeks): e.g., reduce pain enough to sleep")
add_bullet("Long-term goal (4-6 weeks): e.g., return to running")
page_break()
# ════════════════════════════════════════════════════════════════════════════
# SECTION 3 — Objective
# ════════════════════════════════════════════════════════════════════════════
doc.add_heading("Section 3: Step-by-Step — O (Objective Assessment)", level=1)
add_shaded_box("This is data YOU collect, measure, and observe. It must be factual, measurable, and reproducible.")
doc.add_heading("3.1 Observation / Inspection (LOOK — before you touch)", level=2)
add_paragraph("Always compare BOTH sides. Start from head to toe:", size=11)
for item in [
("Posture: ", "Static alignment — any scoliosis, kyphosis, lordosis, valgus/varus"),
("Gait: ", "Watch how the patient walks in — antalgic (painful), Trendelenburg, steppage?"),
("Swelling / Edema: ", "Present? Location? Pitting or non-pitting?"),
("Muscle wasting / Atrophy: ", "Visible asymmetry between limbs"),
("Skin changes: ", "Color (redness=inflammation), scars, bruising, cyanosis"),
("Deformity: ", "Bony or soft tissue — note if present"),
]:
add_bullet(item[1], bold_prefix=item[0])
doc.add_heading("3.2 Palpation (FEEL)", level=2)
for item in [
("Temperature: ", "Warmth = active inflammation. Use back of hand."),
("Tenderness: ", "Point tenderness vs. diffuse. Grade: mild / moderate / severe."),
("Swelling: ", "Fluctuant (fluid) vs. firm (tissue/scar) vs. hard (bone)"),
("Muscle spasm: ", "Involuntary hardness / guarding"),
("Crepitus: ", "Grating or clicking sensation during movement"),
]:
add_bullet(item[1], bold_prefix=item[0])
doc.add_heading("3.3 Range of Motion (ROM) — Use a Goniometer", level=2)
add_paragraph("Always record: Active ROM → Passive ROM → Active Assisted ROM", bold=True, size=11)
tbl2 = doc.add_table(rows=4, cols=4)
tbl2.style = "Table Grid"
tbl2.autofit = False
for i, w in enumerate([1.5, 1.2, 1.2, 2.1]):
tbl2.columns[i].width = Inches(w)
add_table_header_row(tbl2, ["Type of ROM", "Who Moves?", "What You Learn", "End-Feel?"])
rom_rows = [
("Active ROM", "Patient alone", "Willingness + ability", "N/A"),
("Passive ROM", "Therapist alone", "Joint structure integrity", "Yes — note quality"),
("Active Assisted ROM", "Patient + Therapist", "Where help is needed", "N/A"),
]
for i, row in enumerate(rom_rows, start=1):
shade = "E8F4FD" if i % 2 == 0 else None
for j, txt in enumerate(row):
set_cell_text(tbl2.rows[i].cells[j], txt, size=10, shade=shade)
doc.add_paragraph()
add_paragraph("End-Feel Types:", bold=True, size=11)
for item in [
("Soft end-feel: ", "Soft tissue approximation (e.g., knee flexion — calf meets thigh) — NORMAL"),
("Firm end-feel: ", "Capsular/ligamentous stretch (e.g., hip rotation) — NORMAL"),
("Hard end-feel: ", "Bone-to-bone (e.g., elbow extension) — NORMAL"),
("Empty end-feel: ", "Patient stops you due to pain before any resistance — ABNORMAL"),
("Springy end-feel: ", "Rebound — suggests loose body or meniscus — ABNORMAL"),
]:
add_bullet(item[1], bold_prefix=item[0])
doc.add_heading("3.4 Muscle Strength Testing — MRC Scale", level=2)
add_paragraph("Test all relevant muscle groups. Full scale in Section 8.", size=11)
for item in [
"Grade each muscle group 0/5 to 5/5",
"Always compare to contralateral side",
"Note if pain limits the test (document this)",
]:
add_bullet(item)
doc.add_heading("3.5 Neurological Assessment (when nerve involvement suspected)", level=2)
for item in [
("Sensation: ", "Light touch, pinprick, temperature — compare sides"),
("Deep Tendon Reflexes (DTR): ", "Biceps (C5), Brachioradialis (C6), Triceps (C7), Knee jerk (L3/4), Ankle jerk (S1)"),
("Dermatomes: ", "Test sensation in specific skin areas corresponding to nerve roots"),
("Myotomes: ", "Test muscle strength corresponding to specific nerve roots"),
("Upper Motor Neuron signs: ", "Babinski, clonus, hyperreflexia"),
]:
add_bullet(item[1], bold_prefix=item[0])
doc.add_heading("3.6 Special Tests", level=2)
add_paragraph("Condition-specific orthopedic tests. Record as POSITIVE or NEGATIVE and what it indicates:", size=11)
tbl3 = doc.add_table(rows=9, cols=3)
tbl3.style = "Table Grid"
tbl3.autofit = False
for i, w in enumerate([1.8, 1.5, 2.7]):
tbl3.columns[i].width = Inches(w)
add_table_header_row(tbl3, ["Test Name", "Region / Structure", "What a (+) Result Means"])
special_tests = [
("Straight Leg Raise (SLR)", "Lumbar spine / Sciatic nerve", "Nerve root irritation L4-S1"),
("Lachman's Test", "Knee / ACL", "ACL laxity/tear"),
("McMurray's Test", "Knee / Meniscus", "Meniscal pathology"),
("Valgus/Varus Stress Test", "Knee / Collateral ligaments", "MCL or LCL injury"),
("Empty Can Test", "Shoulder / Supraspinatus", "Rotator cuff tear"),
("Hawkins-Kennedy Test", "Shoulder / Impingement", "Subacromial impingement"),
("FABER / FADIR", "Hip / SIJ", "Hip pathology or SIJ dysfunction"),
("Neural Tension Tests (ULTT)", "Upper limb nerve roots", "Neural tension / radiculopathy"),
]
for i, row in enumerate(special_tests, start=1):
shade = "E8F4FD" if i % 2 == 0 else None
for j, txt in enumerate(row):
set_cell_text(tbl3.rows[i].cells[j], txt, size=10, shade=shade)
doc.add_paragraph()
doc.add_heading("3.7 Functional Assessment", level=2)
for item in [
"Sit-to-stand (count reps in 30 seconds, or time to stand)",
"Walking distance (10-metre walk test, 6-minute walk test)",
"Stair climbing ability",
"ADL assessment — dressing, bathing, reaching, lifting",
"Balance — single-leg stance time, Berg Balance Scale",
]:
add_checkbox(item)
doc.add_heading("3.8 Measurements", level=2)
for item in [
"Girth measurement — swelling or atrophy (measure in cm at specified distance from landmark)",
"Limb length — true (ASIS to medial malleolus) vs. apparent (umbilicus to medial malleolus)",
"Vital signs (HR, BP, SpO2, RR) — especially for cardiorespiratory conditions",
]:
add_bullet(item)
page_break()
# ════════════════════════════════════════════════════════════════════════════
# SECTION 4 — Assessment
# ════════════════════════════════════════════════════════════════════════════
doc.add_heading("Section 4: Step-by-Step — A (Assessment & Problem Identification)", level=1)
add_shaded_box(
"This is your CLINICAL THINKING. You interpret all S and O data here. This is the most important section.",
shade_hex="E8F0F9"
)
doc.add_heading("4.1 Four Steps to Identify the Patient's Problems", level=2)
add_paragraph("STEP 1 — List ALL abnormal findings:", bold=True, size=11)
add_paragraph(
"Go through every item in your Subjective and Objective and write down everything that is NOT normal "
"or NOT within expected limits for the patient's age.",
size=11, indent=0.3
)
add_paragraph("STEP 2 — Group related findings into 'Problems':", bold=True, size=11)
add_paragraph(
"A problem = anything that affects the patient's health, function, or quality of life. "
"Use the ICF framework (Section 6) to classify each problem.",
size=11, indent=0.3
)
add_paragraph("STEP 3 — Prioritise by asking:", bold=True, size=11)
for item in [
"Which problem is the patient's MAIN complaint (Chief Complaint)?",
"Which problem is most LIMITING their daily function?",
"Which problem is TREATABLE by physiotherapy?",
"Is there anything that needs urgent MEDICAL REFERRAL?",
]:
add_bullet(item, indent_level=1)
add_paragraph("STEP 4 — Write a Clinical Impression / Working Diagnosis:", bold=True, size=11)
add_paragraph(
'Example: "Patient presents with right L4/L5 lumbar radiculopathy with significant functional '
'limitation in walking (>10 min) and self-care, likely secondary to posterior disc herniation."',
italic=True, size=11, indent=0.3, color_hex="2E75B6"
)
doc.add_heading("4.2 Numbered Problem List Format", level=2)
add_paragraph(
"Always write a numbered list of problems, most important first:",
size=11
)
tbl4 = doc.add_table(rows=6, cols=3)
tbl4.style = "Table Grid"
tbl4.autofit = False
for i, w in enumerate([0.6, 2.4, 3.0]):
tbl4.columns[i].width = Inches(w)
add_table_header_row(tbl4, ["#", "Problem", "Evidence (from S or O)"])
problem_ex = [
("1", "Pain — R knee medial, 7/10", "Patient reports, worse on stairs (S); tender on palpation (O)"),
("2", "Reduced ROM — Knee flexion 90° (N: 135°)", "Measured with goniometer (O)"),
("3", "Muscle weakness — R quadriceps 3/5", "MRC grading (O)"),
("4", "Gait deviation — antalgic gait R side", "Observed on entry (O)"),
("5", "Functional limitation — unable to climb stairs", "Reported by patient (S); confirmed on functional test (O)"),
]
for i, row in enumerate(problem_ex, start=1):
shade = "E8F4FD" if i % 2 == 0 else None
for j, txt in enumerate(row):
set_cell_text(tbl4.rows[i].cells[j], txt, size=10, shade=shade)
page_break()
# ════════════════════════════════════════════════════════════════════════════
# SECTION 5 — Plan
# ════════════════════════════════════════════════════════════════════════════
doc.add_heading("Section 5: Step-by-Step — P (Plan)", level=1)
add_shaded_box("Every item in your Plan must directly address a problem in your Assessment. 1 problem = 1 intervention.")
doc.add_heading("5.1 Goals", level=2)
add_paragraph("Short-Term Goals (STG) — 1 to 2 weeks:", bold=True, size=11)
add_paragraph(
"Goals should be SMART: Specific, Measurable, Achievable, Relevant, Time-bound",
size=11, italic=True, indent=0.2
)
for item in [
"Reduce pain from 7/10 to 4/10 within 2 weeks",
"Increase knee flexion ROM by 20° within 2 weeks",
"Reduce swelling by 1 cm girth measurement at the knee within 1 week",
]:
add_bullet(item)
add_paragraph("Long-Term Goals (LTG) — 4 to 6 weeks:", bold=True, size=11)
for item in [
"Achieve full pain-free ROM (0° extension to 135° flexion)",
"Return to independent stair climbing",
"Return to jogging 30 min without pain",
]:
add_bullet(item)
doc.add_heading("5.2 Treatment Interventions (link each to a problem)", level=2)
tbl5 = doc.add_table(rows=7, cols=3)
tbl5.style = "Table Grid"
tbl5.autofit = False
for i, w in enumerate([1.5, 1.8, 2.7]):
tbl5.columns[i].width = Inches(w)
add_table_header_row(tbl5, ["Problem", "Intervention", "Goal of Intervention"])
plan_data = [
("Pain", "TENS, Ice/Heat, US therapy, Joint mobilization Gr I-II", "Pain modulation, reduce inflammation"),
("Reduced ROM", "Passive stretching, Joint mobilization Gr III-IV, PNF", "Restore joint range"),
("Weakness", "Strengthening: quad sets, SLR, closed-chain progression", "Restore muscle strength to 5/5"),
("Gait deviation", "Gait training, weight-bearing correction, assistive aid", "Normalize walking pattern"),
("Swelling", "Elevation, compression bandage, lymphatic drainage", "Reduce edema"),
("Functional limit", "Stair training, functional retraining, home exercise prog.", "Return to independent ADL"),
]
for i, row in enumerate(plan_data, start=1):
shade = "E8F4FD" if i % 2 == 0 else None
for j, txt in enumerate(row):
set_cell_text(tbl5.rows[i].cells[j], txt, size=10, shade=shade)
doc.add_paragraph()
doc.add_heading("5.3 Frequency, Duration, Reassessment", level=2)
for item in [
"Frequency: e.g., 3 sessions per week",
"Duration: e.g., 4 to 6 weeks",
"Reassessment date: e.g., Reassess after 2 weeks or 6 sessions",
"Home Exercise Programme (HEP): list exercises given to patient",
"Patient education: what you taught the patient (posture, activity modification, red flags)",
"Referral: is any referral needed (medical, psychology, orthotics)?",
]:
add_checkbox(item)
page_break()
# ════════════════════════════════════════════════════════════════════════════
# SECTION 6 — ICF Model
# ════════════════════════════════════════════════════════════════════════════
doc.add_heading("Section 6: How to Identify the Patient's Problem — The ICF Model", level=1)
add_paragraph(
"The WHO International Classification of Functioning, Disability and Health (ICF) is the framework "
"physiotherapy uses to classify and prioritise patient problems. Every problem fits into one of three levels:",
size=11
)
tbl6 = doc.add_table(rows=4, cols=4)
tbl6.style = "Table Grid"
tbl6.autofit = False
for i, w in enumerate([1.5, 1.5, 1.8, 1.2]):
tbl6.columns[i].width = Inches(w)
add_table_header_row(tbl6, ["ICF Level", "Definition", "Example", "Found in SOAP"])
icf_data = [
("Impairment", "Structural or functional deficit", "Reduced ROM, weak muscle, pain", "O and A"),
("Activity Limitation", "Difficulty performing an activity", "Cannot walk, cannot dress self", "S and A"),
("Participation Restriction", "Cannot fulfil social/life roles", "Cannot go to work, cannot play sport", "S and A"),
]
for i, row in enumerate(icf_data, start=1):
shade = "E8F4FD" if i % 2 == 0 else None
for j, txt in enumerate(row):
set_cell_text(tbl6.rows[i].cells[j], txt, size=10, shade=shade)
doc.add_paragraph()
add_shaded_box(
"Where all three levels overlap = your PRIORITY problem. "
"Address all three levels in your Plan, not just the impairment.",
shade_hex="E8F0F9"
)
add_paragraph("To identify the patient's problem, ask three questions:", bold=True, size=11)
for item in [
("What bothers the patient most? ", "→ Chief Complaint (from Subjective)"),
("What is objectively abnormal? ", "→ Abnormal findings (from Objective, compared to normal values)"),
("What is stopping them from living normally? ", "→ Functional and participation impact"),
]:
add_bullet(item[1], bold_prefix=item[0])
page_break()
# ════════════════════════════════════════════════════════════════════════════
# SECTION 7 — OPQRST Cheat Sheet
# ════════════════════════════════════════════════════════════════════════════
doc.add_heading("Section 7: OPQRST Pain Assessment Cheat Sheet", level=1)
tbl7 = doc.add_table(rows=7, cols=4)
tbl7.style = "Table Grid"
tbl7.autofit = False
for i, w in enumerate([0.6, 1.4, 1.8, 2.2]):
tbl7.columns[i].width = Inches(w)
add_table_header_row(tbl7, ["Letter", "Stands For", "Questions to Ask", "What It Tells You"])
opqrst = [
("O", "Onset", "When did it start? Sudden or gradual?", "Trauma vs. overuse / inflammatory vs. mechanical"),
("P", "Provocation/Palliation", "What makes it WORSE? What makes it BETTER?", "Mechanical (worse with movement) vs. inflammatory (better with movement)"),
("Q", "Quality", "Sharp? Dull? Burning? Aching? Throbbing?", "Sharp/shooting = nerve; aching = muscle; burning = nerve"),
("R", "Radiation", "Does pain travel? Where does it go?", "Nerve root referral pattern; somatic vs. radicular"),
("S", "Severity", "Rate 0-10. At rest? During activity? After activity?", "Baseline + functional impact; track over time"),
("T", "Timing", "Constant or intermittent? Morning stiffness? Night pain?","Morning = inflammatory; night pain = serious pathology / flag"),
]
for i, row in enumerate(opqrst, start=1):
shade = "E8F4FD" if i % 2 == 0 else None
for j, txt in enumerate(row):
set_cell_text(tbl7.rows[i].cells[j], txt, size=10, shade=shade)
doc.add_paragraph()
add_shaded_box(
"RED FLAG: Night pain, unexplained weight loss, bowel/bladder changes, bilateral symptoms, saddle anaesthesia "
"= refer to doctor IMMEDIATELY before starting treatment.",
shade_hex="FFE0E0", text_color="8B0000"
)
page_break()
# ════════════════════════════════════════════════════════════════════════════
# SECTION 8 — MRC Scale
# ════════════════════════════════════════════════════════════════════════════
doc.add_heading("Section 8: MRC Muscle Strength Grading Scale", level=1)
tbl8 = doc.add_table(rows=7, cols=3)
tbl8.style = "Table Grid"
tbl8.autofit = False
for i, w in enumerate([0.8, 2.0, 3.2]):
tbl8.columns[i].width = Inches(w)
add_table_header_row(tbl8, ["Grade", "Description", "What You See / How You Test"])
mrc_data = [
("0/5", "No contraction", "No visible or palpable muscle activity at all"),
("1/5", "Flicker / trace contraction", "Visible or palpable flicker but no joint movement"),
("2/5", "Movement with gravity eliminated", "Full ROM but only when gravity is removed (test horizontally)"),
("3/5", "Movement against gravity only", "Full ROM against gravity but no added resistance"),
("4/5", "Movement against some resistance", "Full ROM against gravity + partial external resistance"),
("5/5", "Normal strength", "Full ROM against gravity + full external resistance (normal)"),
]
for i, row in enumerate(mrc_data, start=1):
shade = "E8F4FD" if i % 2 == 0 else None
for j, txt in enumerate(row):
b = (j == 0)
set_cell_text(tbl8.rows[i].cells[j], txt, bold=b, size=10, shade=shade)
doc.add_paragraph()
add_paragraph(
"Note: Grade 4 can be subdivided into 4- (slight resistance), 4 (moderate), and 4+ (strong resistance). "
"Always document which side (L/R) and the movement tested (e.g., R elbow flexion 4/5).",
size=10, color_hex="555555"
)
page_break()
# ════════════════════════════════════════════════════════════════════════════
# SECTION 9 — BLANK TEMPLATE: MUSCULOSKELETAL
# ════════════════════════════════════════════════════════════════════════════
doc.add_heading("Section 9: SOAP Note Blank Template — Musculoskeletal", level=1)
add_shaded_box("PRINT THIS PAGE and fill it in during clinical placement.", shade_hex="E8F4E8", text_color="1A6B1A")
def write_field(label, lines=1):
add_paragraph(label, bold=True, size=10, space_after=1)
for _ in range(lines):
p = doc.add_paragraph()
p.paragraph_format.space_after = Pt(1)
p.paragraph_format.space_before = Pt(1)
run = p.add_run("_" * 90)
run.font.size = Pt(10)
run.font.color.rgb = RGBColor(0xCC,0xCC,0xCC)
# Header box
add_shaded_box("Patient Details", shade_hex="1A4F8A", text_color="FFFFFF")
tbl_h = doc.add_table(rows=2, cols=4)
tbl_h.style = "Table Grid"
tbl_h.autofit = False
for i in range(4): tbl_h.columns[i].width = Inches(1.5)
for r in range(2):
for c in range(4):
tbl_h.rows[r].cells[r*4+c if r*4+c < 8 else 0]
labels = [["Name:", "Age/Sex:", "Occupation:", "Date:"],
["Referred by:", "Diagnosis (if given):", "D/H:", "Dominant side:"]]
for r, row_labels in enumerate(labels):
for c, lbl in enumerate(row_labels):
cell = tbl_h.rows[r].cells[c]
cell.text = ""
p = cell.paragraphs[0]
p.paragraph_format.space_before = Pt(3)
p.paragraph_format.space_after = Pt(3)
rb = p.add_run(lbl)
rb.bold = True; rb.font.size = Pt(9)
p.add_run(" ___________________").font.size = Pt(9)
doc.add_paragraph()
# S section
add_shaded_box("S — SUBJECTIVE", shade_hex="2E75B6", text_color="FFFFFF")
write_field("Chief Complaint (patient's own words):", lines=1)
write_field("Onset (when, how — sudden/gradual):", lines=1)
write_field("Provocation / Palliation (what makes it worse / better):", lines=1)
write_field("Quality of pain (sharp / dull / burning / aching):", lines=1)
write_field("Radiation (does it travel? where?):", lines=1)
write_field("Severity (0-10 at rest:___ / on activity:___ / after activity:___):", lines=1)
write_field("Timing (constant / intermittent / morning stiffness / night pain):", lines=1)
write_field("Past medical / surgical history:", lines=1)
write_field("Drug history:", lines=1)
write_field("Social / occupational history:", lines=1)
write_field("Patient goals:", lines=1)
doc.add_paragraph()
# O section
add_shaded_box("O — OBJECTIVE", shade_hex="2E75B6", text_color="FFFFFF")
write_field("Observation — Posture / Gait / Swelling / Wasting / Deformity:", lines=2)
write_field("Palpation — Temperature / Tenderness / Spasm / Crepitus:", lines=2)
write_field("Active ROM: Flexion:___ Extension:___ Abd:___ Add:___ IR:___ ER:___", lines=1)
write_field("Passive ROM: Flexion:___ Extension:___ Abd:___ Add:___ IR:___ ER:___", lines=1)
write_field("End-feel:", lines=1)
write_field("Muscle strength (MRC): R: ___/5 L: ___/5 Movement tested: _______________", lines=1)
write_field("Sensation / Reflexes / Dermatomes:", lines=1)
write_field("Special tests (name, result +/-):", lines=2)
write_field("Functional tests:", lines=1)
write_field("Measurements (girth, limb length, vitals):", lines=1)
write_field("Investigations reviewed:", lines=1)
doc.add_paragraph()
# A section
add_shaded_box("A — ASSESSMENT", shade_hex="2E75B6", text_color="FFFFFF")
write_field("Clinical Impression / Working Diagnosis:", lines=2)
add_paragraph("Problem List:", bold=True, size=10, space_after=1)
for i in range(1, 6):
write_field(f"Problem {i}:", lines=1)
doc.add_paragraph()
# P section
add_shaded_box("P — PLAN", shade_hex="2E75B6", text_color="FFFFFF")
write_field("Short-Term Goals (1-2 weeks):", lines=2)
write_field("Long-Term Goals (4-6 weeks):", lines=2)
write_field("Treatment Interventions:", lines=3)
write_field("Frequency: ___ sessions/week Duration: ___ weeks", lines=1)
write_field("Home Exercise Programme (HEP):", lines=2)
write_field("Patient education:", lines=1)
write_field("Reassessment date:", lines=1)
page_break()
# ════════════════════════════════════════════════════════════════════════════
# SECTION 10 — BLANK TEMPLATE: NEUROLOGICAL
# ════════════════════════════════════════════════════════════════════════════
doc.add_heading("Section 10: SOAP Note Blank Template — Neurological", level=1)
add_shaded_box("Use this template for stroke, spinal cord injury, TBI, cerebral palsy, Parkinson's cases.", shade_hex="E8F4E8", text_color="1A6B1A")
add_shaded_box("S — SUBJECTIVE (Neurological)", shade_hex="2E75B6", text_color="FFFFFF")
write_field("Chief complaint:", lines=1)
write_field("Onset — date, cause (stroke/trauma/progressive):", lines=1)
write_field("Affected side (L / R / bilateral):", lines=1)
write_field("Functional complaint (what can't they do?):", lines=2)
write_field("Past medical history (HTN, DM, cardiac, seizures):", lines=1)
write_field("Drug history:", lines=1)
write_field("Patient/caregiver goals:", lines=1)
doc.add_paragraph()
add_shaded_box("O — OBJECTIVE (Neurological)", shade_hex="2E75B6", text_color="FFFFFF")
write_field("Consciousness / Cognition (GCS, orientation, memory):", lines=1)
write_field("Communication (speech, comprehension, aphasia):", lines=1)
write_field("Tone — Hypotonia / Hypertonia / Spasticity (Modified Ashworth Scale: ___/4):", lines=1)
write_field("Sensation — Light touch / Pinprick / Temperature / Proprioception:", lines=1)
write_field("Reflexes — Deep tendon / Pathological (Babinski, clonus):", lines=1)
write_field("Motor control — voluntary movement, coordination, tremor:", lines=1)
write_field("Balance — Static sitting / Standing / Berg Balance Scale: ___ /56:", lines=1)
write_field("Gait — pattern, assistive device, supervision needed:", lines=1)
write_field("ROM (affected limb):", lines=1)
write_field("Muscle strength (MRC where applicable):", lines=1)
write_field("Functional scores — Barthel Index: ___ /100 FIM: ___/126:", lines=1)
doc.add_paragraph()
add_shaded_box("A — ASSESSMENT (Neurological)", shade_hex="2E75B6", text_color="FFFFFF")
write_field("Clinical impression / neurological diagnosis:", lines=2)
add_paragraph("Problem List:", bold=True, size=10, space_after=1)
for i in range(1, 6):
write_field(f"Problem {i}:", lines=1)
doc.add_paragraph()
add_shaded_box("P — PLAN (Neurological)", shade_hex="2E75B6", text_color="FFFFFF")
write_field("STG (1-2 weeks):", lines=2)
write_field("LTG (4-8 weeks):", lines=2)
write_field("Neurological interventions (task-specific training, NDT, PNF, treadmill, FES):", lines=3)
write_field("Frequency / Duration:", lines=1)
write_field("HEP and caregiver training:", lines=2)
write_field("Reassessment date:", lines=1)
page_break()
# ════════════════════════════════════════════════════════════════════════════
# SECTION 11 — PRE-ASSESSMENT CHECKLIST
# ════════════════════════════════════════════════════════════════════════════
doc.add_heading("Section 11: Pre-Assessment Checklist", level=1)
add_paragraph("Use this checklist BEFORE you start every assessment to make sure you don't miss anything:", size=11)
add_shaded_box("Before the Patient Enters", shade_hex="E8F0F9", text_color="1A4F8A")
for item in [
"Read the referral / case notes",
"Note any contraindications (e.g., deep vein thrombosis, fracture not yet healed, post-op precautions)",
"Prepare your goniometer, tape measure, reflex hammer, pen torch, tendon hammer",
"Have your SOAP note template ready",
]:
add_checkbox(item)
add_shaded_box("During Subjective (S)", shade_hex="E8F0F9", text_color="1A4F8A")
for item in [
"Introduced yourself and explained your role",
"Obtained verbal consent",
"Screened for red flags (cauda equina, malignancy, fracture, infection)",
"Used OPQRST for all pain complaints",
"Asked about goals",
]:
add_checkbox(item)
add_shaded_box("During Objective (O)", shade_hex="E8F0F9", text_color="1A4F8A")
for item in [
"Observed before touching (posture, gait, swelling, asymmetry)",
"Compared both sides for all measurements",
"Used a goniometer for all ROM measurements",
"Graded all muscle strength using MRC scale",
"Performed and recorded all relevant special tests",
"Measured any swelling with tape measure",
"Reviewed investigations (X-ray, MRI, blood reports)",
]:
add_checkbox(item)
add_shaded_box("During Assessment (A)", shade_hex="E8F0F9", text_color="1A4F8A")
for item in [
"Listed all abnormal findings from S and O",
"Written a numbered problem list (most important first)",
"Classified problems using ICF (impairment / activity / participation)",
"Written a clear clinical impression sentence",
]:
add_checkbox(item)
add_shaded_box("During Plan (P)", shade_hex="E8F0F9", text_color="1A4F8A")
for item in [
"Written SMART short-term and long-term goals",
"Linked each intervention to a specific problem",
"Stated frequency and duration of treatment",
"Documented HEP given to patient",
"Documented patient education provided",
"Stated reassessment date",
"Identified any referral needed",
]:
add_checkbox(item)
page_break()
# ════════════════════════════════════════════════════════════════════════════
# SECTION 12 — WORKED EXAMPLE
# ════════════════════════════════════════════════════════════════════════════
doc.add_heading("Section 12: Worked Example — Right Knee Pain Patient", level=1)
add_shaded_box("Read this example fully before writing your first real SOAP note.", shade_hex="FFF8E1", text_color="7B4F00")
# S
add_shaded_box("S — SUBJECTIVE", shade_hex="1A4F8A", text_color="FFFFFF")
add_paragraph(
'Chief Complaint: "My right knee hurts when I go up and down stairs and I cannot run anymore."',
italic=True, size=11
)
add_paragraph("HPI:", bold=True, size=11)
for item in [
"Onset: 3 months ago, gradual onset. No trauma. Started after increasing running distance.",
"Provocation: Worse on stairs (going down > going up), sitting for long periods, squatting. Better with rest and ice.",
"Quality: Dull aching, occasionally sharp with sudden movements.",
"Radiation: No radiation. Localised to anterior knee around patella.",
"Severity: 6/10 on stairs, 3/10 at rest.",
"Timing: Worse in the morning (first 10 min). Improves with gentle movement. Flares after running.",
]:
add_bullet(item)
add_paragraph("PMH: No previous knee injury. Mild obesity (BMI 28). No surgeries.", size=11)
add_paragraph("Drug history: Takes ibuprofen occasionally.", size=11)
add_paragraph("Social: Software engineer, desk job 8 hrs/day. Recreational runner 4x/week.", size=11)
add_paragraph("Goal: Return to running 5 km without pain within 6 weeks.", size=11)
doc.add_paragraph()
# O
add_shaded_box("O — OBJECTIVE", shade_hex="1A4F8A", text_color="FFFFFF")
add_paragraph("Observation:", bold=True, size=11)
for item in [
"Mild swelling around the right patella compared to left.",
"Slight quadriceps wasting visible on right thigh.",
"Gait: Mild antalgic pattern with reduced weight-bearing on right.",
"Posture: Bilateral genu valgum (mild).",
]:
add_bullet(item)
add_paragraph("Palpation:", bold=True, size=11)
for item in [
"Tenderness: Point tenderness on medial patellar facet (3/10 pressure pain).",
"Temperature: Slight warmth around right patella.",
"Crepitus: Present with knee flexion/extension.",
]:
add_bullet(item)
add_paragraph("ROM (goniometer):", bold=True, size=11)
for item in [
"Active: R knee flexion 110° (normal 135°), extension 0° (normal).",
"Passive: R knee flexion 120°, end-feel — firm/capsular.",
"L knee (unaffected): Flexion 135°, extension 0° — normal.",
]:
add_bullet(item)
add_paragraph("Muscle Strength (MRC):", bold=True, size=11)
for item in [
"R quadriceps: 3+/5 (limited by pain). L quadriceps: 5/5.",
"R hip abductors: 4/5. L: 5/5.",
]:
add_bullet(item)
add_paragraph("Special Tests:", bold=True, size=11)
for item in [
"Clarke's sign (patellar grind test): POSITIVE — pain with patellar compression = patellofemoral pathology.",
"Valgus stress test: NEGATIVE (MCL intact).",
"Lachman's test: NEGATIVE (ACL intact).",
"McMurray's: NEGATIVE (menisci intact).",
]:
add_bullet(item)
add_paragraph("Girth measurement:", bold=True, size=11)
for item in [
"R knee at mid-patella: 36 cm. L knee: 34 cm. (2 cm swelling on right.)",
]:
add_bullet(item)
doc.add_paragraph()
# A
add_shaded_box("A — ASSESSMENT", shade_hex="1A4F8A", text_color="FFFFFF")
add_paragraph(
'Clinical Impression: "Patient presents with right patellofemoral pain syndrome (PFPS) with moderate '
'functional limitation in stair use and running, in the context of quadriceps weakness, mild genu valgum, and overuse."',
italic=True, size=11
)
add_paragraph("Problem List:", bold=True, size=11)
for item in [
"1. Pain — R anterior knee 6/10, worse on stairs and running (primary complaint) [Impairment]",
"2. Reduced ROM — R knee flexion limited to 110° (deficit: 25°) [Impairment]",
"3. Muscle weakness — R quadriceps 3+/5, R hip abductors 4/5 [Impairment]",
"4. Swelling — 2 cm girth excess at R knee [Impairment]",
"5. Functional limitation — unable to run, difficulty with stairs [Activity Limitation]",
"6. Participation restriction — cannot engage in recreational running [Participation Restriction]",
]:
add_bullet(item)
doc.add_paragraph()
# P
add_shaded_box("P — PLAN", shade_hex="1A4F8A", text_color="FFFFFF")
add_paragraph("STG (2 weeks): Reduce pain to 3/10. Increase flexion to 125°. Reduce swelling by 1 cm.", size=11)
add_paragraph("LTG (6 weeks): Full pain-free ROM. Quadriceps 5/5. Return to running 5 km.", size=11)
add_paragraph("Interventions:", bold=True, size=11)
for item in [
"Pain/swelling: Cryotherapy post-exercise, patellar taping (McConnell), TENS.",
"ROM: Passive stretching of quadriceps and ITB, joint mobilisation Grade I-II.",
"Strengthening: Quad sets → SLR → short arc quads → VMO biofeedback training → closed chain progression.",
"Hip: Hip abductor and external rotator strengthening (clam exercises, SL bridges).",
"Gait retraining: Reduce step width, improve knee alignment during loading.",
]:
add_bullet(item)
add_paragraph("Frequency: 3x/week for 6 weeks.", size=11)
add_paragraph("HEP: Quad sets 3x10, SLR 3x10, ITB stretch 30 sec x3 — daily.", size=11)
add_paragraph("Education: Explain PFPS mechanism, activity modification (reduce downhill running), footwear advice.", size=11)
add_paragraph("Reassessment: After 2 weeks (6 sessions).", size=11)
page_break()
# ════════════════════════════════════════════════════════════════════════════
# SECTION 13 — COMMON MISTAKES
# ════════════════════════════════════════════════════════════════════════════
doc.add_heading("Section 13: Common Mistakes to Avoid", level=1)
mistakes = [
("Mixing S and O sections",
"Keep them strictly separate. If the patient said it = S. If you measured it = O."),
("Vague language in O",
"WRONG: 'Patient has reduced ROM.' RIGHT: 'Active R knee flexion = 90° (normal 135°, deficit 45°).'"),
("Writing only a diagnosis in A",
"Always include FUNCTIONAL IMPACT. The problem list must reflect what the patient CANNOT DO, not just the tissue diagnosis."),
("Not prioritising the problem list",
"Put the most functionally limiting problem first — this drives your treatment priority."),
("Not comparing both sides",
"Every ROM, strength, and measurement MUST be compared to the opposite limb or age-matched norms."),
("Forgetting patient goals",
"Your Plan must include goals the patient actually cares about, not just what you think is important."),
("Writing interventions without linking to problems",
"Every intervention must address a specific problem in your problem list (P must answer A)."),
("Ignoring red flags",
"Always screen: night pain, unexplained weight loss, bilateral limb symptoms, bowel/bladder changes = urgent referral."),
("Not documenting measurements at reassessment",
"You cannot show progress without baseline measurements. Always record numbers."),
]
tbl_m = doc.add_table(rows=len(mistakes)+1, cols=2)
tbl_m.style = "Table Grid"
tbl_m.autofit = False
tbl_m.columns[0].width = Inches(2.0)
tbl_m.columns[1].width = Inches(4.0)
add_table_header_row(tbl_m, ["Common Mistake", "How to Avoid It"])
for i, (mistake, fix) in enumerate(mistakes, start=1):
shade = "FFE8E8" if i % 2 != 0 else "FFF0F0"
set_cell_text(tbl_m.rows[i].cells[0], mistake, bold=True, size=10, shade=shade)
set_cell_text(tbl_m.rows[i].cells[1], fix, size=10, shade=shade)
doc.add_paragraph()
add_shaded_box(
"Remember: A well-written SOAP note tells the complete story of the patient in 1-2 pages. "
"Another therapist should be able to pick up your notes and continue treatment without asking you a single question.",
shade_hex="E8F4E8", text_color="1A6B1A"
)
# ── Save ─────────────────────────────────────────────────────────────────────
doc.save(OUTPUT)
print(f"Saved: {OUTPUT}")
cd /tmp/workspace/physio-study-guide && python3 build_guide.py
~/physio-study-guide/build_guide.py
Successfully replaced 1 instance(s) of the string in '/tmp/workspace/physio-study-guide/build_guide.py'
cd /tmp/workspace/physio-study-guide && python3 build_guide.py
ls -lh /tmp/workspace/physio-study-guide/Physiotherapy_SOAP_Study_Guide.docx
Physiotherapy SOAP Study Guide
Word Document · DOCX
| Section | Content |
|---|---|
| 1 | What a SOAP note is — summary table, key principles |
| 2 | S (Subjective) step-by-step with checkboxes |
| 3 | O (Objective) — observation, palpation, ROM, strength, special tests, neurology |
| 4 | A (Assessment) — 4-step method to identify & prioritise problems, sample problem list |
| 5 | P (Plan) — SMART goals, intervention table, frequency/duration |
| 6 | ICF Model — how to classify every problem at 3 levels |
| 7 | OPQRST cheat sheet (full table with what each answer tells you) + Red Flags box |
| 8 | MRC Muscle Grading Scale 0-5 (full table) |
| 9 | Blank printable template — Musculoskeletal (fillable lines) |
| 10 | Blank printable template — Neurological (fillable lines) |
| 11 | Pre-assessment checklist (before patient / during S / during O / during A / during P) |
| 12 | Full worked example — right knee pain patient (complete SOAP note) |
| 13 | 9 common mistakes students make + how to fix each one |