# Exceptions ג': raise — לזרוק שגיאה בעצמנו<no value>

## מה נלמד?

עד עכשיו **Python** זרקה שגיאות, ואנחנו **תפסנו**. היום נלמד לזרוק בעצמנו, עם **`raise`**, כשהקוד שלנו מזהה מצב לא חוקי.

## למה צריך את זה?

נניח שכתבתם פונקציה `withdraw(balance, amount)` למשיכה מחשבון בנק. מה היא צריכה לעשות כשמבקשים למשוך **מינוס 500**? אפשרויות:

1. **לבצע בשקט** — היתרה **גדלה** ב-500. באג כספי חמור.
2. **להדפיס "error"** — אבל מי שקרא לפונקציה לא יודע שהיא נכשלה, וממשיך לעבוד עם תוצאה שגויה.
3. **להחזיר ערך מיוחד** (כמו `-1` או `None`) — מי שקרא חייב לזכור לבדוק אותו. אם שכח, הבאג "זולג".
4. **לזרוק שגיאה** — מי שקרא **חייב** להתייחס אליה, או שהתוכנית תעצור עם הודעה ברורה בדיוק במקום הבעייתי.

אפשרות 4 היא הדרך המקצועית. בנושא OOP נשתמש בה ל-**Validation** בתוך מחלקות, ובנושא Backend ומסדי נתונים שרת FastAPI יהפוך שגיאות כאלה לתשובת HTTP `400` או `404`.

## הסבר מהבסיס

### `raise`

`raise` **זורקת** Exception, בדיוק כמו ש-Python עושה כשמשהו נשבר. מרגע ה-`raise`:

- הפונקציה **מפסיקה מיד**. שום שורה אחריה לא רצה, ושום ערך לא מוחזר.
- השגיאה "עולה" למי שקרא לפונקציה. אם הוא תופס אותה עם `except`, מצוין. אם לא, היא ממשיכה לעלות, ובסוף התוכנית קורסת עם Traceback.

### החלוקה הנכונה: מי מזהה ומי מטפל

| תפקיד | מי | מה הוא עושה |
|---|---|---|
| **מזהה** את הבעיה | הפונקציה שמקבלת את הנתונים | בודקת ו**זורקת** (`raise`) |
| **מחליט** מה לעשות | מי שקרא לפונקציה | **תופס** (`except`) ומחליט: הודעה, ניסיון חוזר, ברירת מחדל |

{{< box "analogy" "אנלוגיה: מחסן במסעדה" >}}
המלצר (מי שקרא) מבקש מהמחסנאי (הפונקציה) 5 עגבניות. אין עגבניות. מחסנאי טוב **לא** מביא בשקט 5 תפוחים (ערך שגוי), ולא "מחליט" לבד לשנות את המנה. הוא **מודיע** (`raise`): "אין עגבניות!". והמלצר, שמכיר את הלקוח, מחליט: להציע מנה אחרת או להתנצל. כל אחד אחראי לתחום שלו.
{{< /box >}}

### איזה סוג שגיאה לזרוק?

משתמשים בסוגים המובנים, לפי משמעות:

| סוג | מתי |
|---|---|
| `ValueError` | הטיפוס נכון, אבל **הערך** לא חוקי: גיל שלילי, סכום אפס, מייל בלי `@` |
| `TypeError` | **הטיפוס** לא נכון: התקבל טקסט במקום מספר |
| `KeyError` / `LookupError` | חיפוש של משהו שלא קיים |
| `PermissionError` | אין הרשאה לפעולה |

ההודעה צריכה להסביר **מה לא בסדר ולמה**: `"amount must be positive, got -500"`, ולא `"error"`.

(בנושא OOP, אחרי שנלמד מחלקות, נראה איך יוצרים **סוגי שגיאה משלנו**, כמו `InsufficientFundsError`.)

## תחביר

```python
raise ValueError("clear message about what is wrong")

# typical pattern: validate at the start of a function
def function_name(value):
    if not is_valid(value):
        raise ValueError("why it is invalid")
    # ... normal work, only with valid data

# the caller decides
try:
    function_name(bad_value)
except ValueError as error:
    print("Problem:", error)

# re-raise: handle partly, then pass the same error up
except ValueError:
    print("logging the problem...")
    raise
```

## דוגמת קוד ראשונה

`week-2/withdraw.py`:

```python
def withdraw(balance, amount):
    if amount <= 0:
        raise ValueError("amount must be positive, got " + str(amount))
    if amount > balance:
        raise ValueError("insufficient funds: balance " + str(balance) + ", requested " + str(amount))
    return balance - amount


print(withdraw(1000, 300))
print(withdraw(1000, -500))
print("this line never runs")
```

**פלט:**

```text
700
Traceback (most recent call last):
  File "withdraw.py", line 10, in <module>
    print(withdraw(1000, -500))
  File "withdraw.py", line 3, in withdraw
    raise ValueError("amount must be positive, got " + str(amount))
ValueError: amount must be positive, got -500
```

## פירוק הקוד שורה אחר שורה

| שורה | הסבר |
|---|---|
| `if amount <= 0:` | **Validation** (אימות קלט): בודקים לפני שעושים משהו. |
| `raise ValueError("...")` | יוצרים אובייקט `ValueError` עם הודעה, וזורקים אותו. הפונקציה נעצרת כאן. |
| `if amount > balance:` | בדיקה שנייה, לכלל עסקי אחר. |
| `return balance - amount` | מגיעים לכאן **רק** אם כל הבדיקות עברו. מכאן והלאה, הנתונים בטוחים. |
| `withdraw(1000, 300)` | עובר את הבדיקות ומחזיר 700. |
| `withdraw(1000, -500)` | נזרקת שגיאה. אף אחד לא תופס אותה, ולכן התוכנית קורסת עם Traceback ברור. השורה האחרונה בקוד לא רצה. |

**שימו לב לאיכות ה-Traceback:** ההודעה אומרת בדיוק **מה** הבעיה (`amount must be positive`) ו**איזה ערך** גרם לה (`-500`). כך נראית שגיאה טובה.

## דוגמה נוספת

**מי שקרא מחליט.** אותה פונקציה, עם קוד שתופס ומטפל. `week-2/atm_safe.py`:

```python
def withdraw(balance, amount):
    if amount <= 0:
        raise ValueError("amount must be positive, got " + str(amount))
    if amount > balance:
        raise ValueError("insufficient funds: balance " + str(balance) + ", requested " + str(amount))
    return balance - amount


balance = 1000
requests = [200, -50, 5000, 300]

for amount in requests:
    try:
        balance = withdraw(balance, amount)
    except ValueError as error:
        print("Rejected:", error)
    else:
        print("Withdrew", amount, "- new balance:", balance)

print("Final balance:", balance)
```

**פלט:**

```text
Withdrew 200 - new balance: 800
Rejected: amount must be positive, got -50
Rejected: insufficient funds: balance 800, requested 5000
Withdrew 300 - new balance: 500
Final balance: 500
```

**פירוק:**

- `withdraw` לא מדפיסה כלום ולא יודעת מה יקרה אחר כך. היא רק **מזהה** ו**זורקת**.
- הלולאה (מי שקרא) **מחליטה**: הבקשה נדחית עם הודעה, והלולאה ממשיכה לבקשה הבאה.
- כשהשגיאה נזרקת, ההשמה `balance = withdraw(...)` **לא מתבצעת**, ולכן היתרה נשארת כמו שהייתה. זה בדיוק מה שרצינו.
- מחר אותה `withdraw` יכולה לשמש באתר, באפליקציה או בשרת, וכל אחד מהם יטפל בשגיאה בדרך שלו (הודעה על המסך, תשובת HTTP `400`...).

### `raise` בלי שום דבר אחריו — העברה הלאה

לפעמים רוצים לעשות **משהו** כשיש שגיאה (למשל לרשום אותה), אבל **לא** "להעלים" אותה:

```python
try:
    balance = withdraw(balance, 5000)
except ValueError:
    print("Audit: a failed withdrawal attempt was recorded")
    raise
```

`raise` לבדו, בתוך `except`, זורק **מחדש את אותה שגיאה בדיוק**. היא ממשיכה לעלות למי שמעל.

## טעות נפוצה

**הטעות 1: `print` במקום `raise`.**

```python
def withdraw(balance, amount):
    if amount > balance:
        print("Not enough money!")
    return balance - amount
```

הפונקציה **מדפיסה** הודעה, אבל **ממשיכה** ומחזירה יתרה שלילית! מי שקרא לא יודע שמשהו השתבש. ההדפסה גם "תקועה" למסך, ואין לה משמעות בשרת או באפליקציה.

**הטעות 2: הודעה לא מועילה.**

```python
raise ValueError("error")
raise ValueError("bad input")
```

איזה input? מה היה הערך? מה מצופה? הודעה טובה = **מה התנאי** + **מה התקבל**: `"age must be between 0 and 120, got 150"`.

**הטעות 3: לזרוק את הסוג הכללי ביותר.**

```python
raise Exception("amount must be positive")
```

`Exception` הוא אב של כמעט כל השגיאות. מי שירצה לתפוס **רק** את הבעיה הזו, יצטרך `except Exception`, שתופס גם באגים לא קשורים. בחרו סוג ספציפי (`ValueError`).

**הטעות 4: לתפוס ולזרוק, ולאבד את ההקשר.**

```python
except ValueError:
    raise ValueError("something failed")
```

ההודעה המקורית (עם הפרטים) הלכה לאיבוד. אם רוצים להעביר הלאה, `raise` לבד שומר על השגיאה המקורית.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## תרגול

התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.

### תרגיל 1: גיל תקין

כתבו פונקציה `set_age(age)` שמחזירה את הגיל אם הוא בין 0 ל-120, ואחרת **זורקת** `ValueError` עם ההודעה שבפלט הצפוי (כולל הגיל שהתקבל). אחר כך כתבו לולאה שמנסה את הגילים `[25, -3, 150]`: לגיל תקין היא מדפיסה `ok:` ואת הגיל, ולשגיאה — `error:` ואת ההודעה.

**פלט צפוי:**

```text
ok: 25
error: age must be between 0 and 120, got -3
error: age must be between 0 and 120, got 150
```

{{< solution >}}
```python
MIN_AGE = 0
MAX_AGE = 120


def set_age(age):
    if age < MIN_AGE or age > MAX_AGE:
        raise ValueError("age must be between 0 and 120, got " + str(age))
    return age


for age in [25, -3, 150]:
    try:
        print("ok:", set_age(age))
    except ValueError as error:
        print("error:", error)
```

```text
ok: 25
error: age must be between 0 and 120, got -3
error: age must be between 0 and 120, got 150
```

הפונקציה רק **מזהה** את הבעיה וזורקת. מי שקרא לה — הלולאה — **מחליט** מה לעשות.
{{< /solution >}}

### תרגיל 2: שם משתמש

כתבו פונקציה `validate_username(name)` שזורקת `ValueError`:

- אם השם קצר מ-3 תווים — עם ההודעה `too short`;
- אם יש בו רווח — עם ההודעה `no spaces allowed`.

אם השם תקין — היא מחזירה אותו.

```python
for name in ["dana", "al", "big boss"]:
    try:
        print("valid:", validate_username(name))
    except ValueError as error:
        print(name, "->", error)
```

**פלט צפוי:**

```text
valid: dana
al -> too short
big boss -> no spaces allowed
```

{{< solution >}}
```python
MIN_USERNAME_LENGTH = 3


def validate_username(name):
    if len(name) < MIN_USERNAME_LENGTH:
        raise ValueError("too short")
    if " " in name:
        raise ValueError("no spaces allowed")
    return name


for name in ["dana", "al", "big boss"]:
    try:
        print("valid:", validate_username(name))
    except ValueError as error:
        print(name, "->", error)
```

```text
valid: dana
al -> too short
big boss -> no spaces allowed
```
{{< /solution >}}

### תרגיל 3: מי שקרא — מחליט

הפונקציה הבאה זורקת שגיאה על מחיר שלילי:

```python
def apply_discount(price, percent):
    if price < 0:
        raise ValueError("price cannot be negative")
    return price - price * percent / 100
```

כתבו שתי פונקציות שמשתמשות בה בשתי דרכים שונות:

1. `report_price(price)` — מדפיסה את המחיר אחרי 10% הנחה, או `invalid price` אם הייתה שגיאה.
2. `price_or_zero(price)` — מחזירה את המחיר אחרי 10% הנחה, או 0 אם הייתה שגיאה.

```python
report_price(200)
report_price(-5)
print(price_or_zero(50))
print(price_or_zero(-5))
```

**פלט צפוי:**

```text
180.0
invalid price
45.0
0
```

{{< solution >}}
```python
def apply_discount(price, percent):
    if price < 0:
        raise ValueError("price cannot be negative")
    return price - price * percent / 100


def report_price(price):
    try:
        print(apply_discount(price, 10))
    except ValueError:
        print("invalid price")


def price_or_zero(price):
    try:
        return apply_discount(price, 10)
    except ValueError:
        return 0


report_price(200)
report_price(-5)
print(price_or_zero(50))
print(price_or_zero(-5))
```

```text
180.0
invalid price
45.0
0
```

אותה פונקציה, שני טיפולים שונים. בגלל זה `apply_discount` **זורקת** ולא מדפיסה בעצמה: היא לא יודעת מה הקורא צריך.
{{< /solution >}}

### תרגיל 4: return None מול raise

שתי גרסאות של אותה פונקציה:

```text
# A                                   # B
def withdraw(balance, amount):        def withdraw(balance, amount):
    if amount > balance:                  if amount > balance:
        print("Not enough money!")            raise ValueError("not enough money")
        return None                       return balance - amount
    return balance - amount
```

מישהו כתב: `balance = withdraw(100, 500)`. מה יקרה בכל גרסה, ולמה B עדיפה?

{{< solution >}}
- **A**: מודפס "Not enough money!", ו-`balance` מקבל **`None`**. התוכנית ממשיכה עם יתרה `None`, ותקרוס מאוחר יותר במקום אחר — רחוק מהבעיה האמיתית.
- **B**: נזרקת `ValueError` **בדיוק במקום** של הבעיה. אם הקורא לא טיפל בה, ה-Traceback מצביע ישר על המשיכה הבעייתית. אם טיפל — הוא מחליט בעצמו מה להציג.
- B גם לא מדפיסה בעצמה: אולי הקורא הוא שרת שצריך להחזיר הודעה, ולא להדפיס למסך.
{{< /solution >}}

### תרגיל 5: המרת ציון

כתבו פונקציה `parse_grade(text)` שממירה טקסט לציון. היא זורקת `ValueError` אם הטקסט אינו מספר (זה קורה כבר ב-`int`), או אם הציון מחוץ לטווח 0 עד 100 — עם ההודעה שבפלט הצפוי (כולל הציון שהתקבל).

```python
for text in ["87", "abc", "120"]:
    try:
        print("grade:", parse_grade(text))
    except ValueError as error:
        print("rejected:", error)
```

**פלט צפוי:**

```text
grade: 87
rejected: invalid literal for int() with base 10: 'abc'
rejected: grade must be 0-100, got 120
```

{{< solution >}}
```python
def parse_grade(text):
    grade = int(text)
    if grade < 0 or grade > 100:
        raise ValueError("grade must be 0-100, got " + str(grade))
    return grade


for text in ["87", "abc", "120"]:
    try:
        print("grade:", parse_grade(text))
    except ValueError as error:
        print("rejected:", error)
```

```text
grade: 87
rejected: invalid literal for int() with base 10: 'abc'
rejected: grade must be 0-100, got 120
```

`parse_grade` לא תופסת את השגיאה של `int` — היא נותנת לה לעבור הלאה לקורא, בדיוק כמו את השגיאה שהיא זורקת בעצמה.
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- `raise SomeError("message")` זורק שגיאה. הפונקציה נעצרת מיד, ושום ערך לא מוחזר.
- **הפונקציה מזהה וזורקת. מי שקרא תופס ומחליט.**
- `ValueError` לערך לא חוקי, `TypeError` לטיפוס לא נכון. לא `Exception` כללי.
- הודעה טובה = מה הכלל + מה התקבל.
- `raise` לבד בתוך `except` מעביר את **אותה** שגיאה הלאה.
- לזרוק עדיף על להדפיס "error" או להחזיר ערך מיוחד, כי שגיאה אי אפשר "לשכוח לבדוק".

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## דוקומנטציה רשמית

- [Python Tutorial — Raising Exceptions](https://docs.python.org/3/tutorial/errors.html#raising-exceptions) — raise
- [The raise statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) — הגדרת raise בשפה

<!-- docs:end -->
