{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "# Boolean operators and truthiness\n\nIn this tutorial, you will go beyond `if`/`elif`/`else` to understand how Python evaluates boolean expressions, what counts as truthy or falsy, and how to use these properties idiomatically.\n\n**Time commitment:** 15–20 minutes\n\n**Prerequisites:**\n\n- Completion of [If statements](https://agilearn.co.uk/guides/conditional-logic/learn/01-if-statements)\n- Comfort with Python variables, strings, numbers, and lists\n\n## Learning objectives\n\nBy the end of this tutorial, you will be able to:\n\n- Use `and`, `or`, and `not` and explain short-circuit evaluation\n- Explain what `and` and `or` actually return (it isn't always `True` or `False`)\n- Identify Python's falsy values\n- Choose between idiomatic truthiness checks and explicit comparisons"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## A quick recap\n\nYou met `and`, `or`, and `not` in the previous tutorial. They combine boolean expressions:\n\n- `and` is true when **both** sides are true\n- `or` is true when **at least one** side is true\n- `not` flips a boolean to its opposite"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "is_member = True\nhas_paid = False\n\nprint(is_member and has_paid)  # False\nprint(is_member or has_paid)   # True\nprint(not has_paid)            # True"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "That much was the headline. The interesting story is in the details — particularly *how* and *when* Python evaluates each side of an expression."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Short-circuit evaluation\n\nPython evaluates `and` and `or` **left to right** and **stops as soon as the answer is decided**. This is called **short-circuit evaluation**.\n\nFor `and`, as soon as Python finds a falsy value, it knows the whole expression is false — there is no point checking the rest.\n\nFor `or`, as soon as Python finds a truthy value, the whole expression is true — again, no need to check the rest."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "def expensive_check():\n    print(\"expensive_check() ran\")\n    return True\n\n# expensive_check is never called — `False and ...` is already False\nprint(False and expensive_check())\nprint()\n\n# expensive_check is never called — `True or ...` is already True\nprint(True or expensive_check())"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "This matters in two practical ways. First, you can avoid wasted work — put the cheap check first. Second, you can use it as a guard against errors:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "user = None\n\n# Without short-circuiting, `user.name` would raise AttributeError\n# With short-circuiting, Python stops at `user` (which is falsy) and returns it\ndisplay_name = user and user.name\nprint(display_name)  # None"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## `and` and `or` return one of their operands\n\nThis is the part that surprises most people: **`and` and `or` do not return `True` or `False`.** They return whichever operand decided the outcome.\n\nFor `or`, that's the first truthy value (or the last value, if all are falsy).\nFor `and`, it's the first falsy value (or the last value, if all are truthy)."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "print(\"\" or \"default\")        # \"default\"  — first was falsy, returned the second\nprint(\"hello\" or \"default\")   # \"hello\"    — first was truthy, returned it\nprint(0 and 1)                # 0          — first was falsy, returned it\nprint(1 and 2)                # 2          — first was truthy, returned the second"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "When an `if` statement uses these expressions, Python coerces the result to a boolean — but the underlying value is what was returned.\n\nThis behaviour is the foundation of a very common idiom:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "raw_input = \"\"  # imagine this came from a form field\n\nname = raw_input or \"Anonymous\"\nprint(name)  # \"Anonymous\" — empty string was falsy, fell through to default"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Truthiness: what counts as true?\n\nPython lets you put almost anything in an `if` condition. Behind the scenes, it converts the value to a boolean using a small set of rules.\n\nThe **falsy** values in Python are:\n\n- `False`\n- `None`\n- Numeric zero: `0`, `0.0`, `0j`\n- Empty sequences: `\"\"`, `()`, `[]`\n- Empty mappings and sets: `{}`, `set()`\n- Empty bytes: `b\"\"`\n\n**Everything else is truthy.** That's the whole list.\n\nTry a few in the cell below and see for yourself:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "for value in [0, 1, -1, \"\", \"hello\", [], [0], None, False, True, 0.0, 0.1]:\n    if value:\n        print(f\"{value!r:10}  -> truthy\")\n    else:\n        print(f\"{value!r:10}  -> falsy\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Idiomatic truthiness checks\n\nPython programmers use truthiness all the time. It makes conditions concise and reads almost like English.\n\n```python\nif items:           # \"if there are any items...\"\nif not name:        # \"if name is missing or empty...\"\nif errors:          # \"if any errors were collected...\"\n```\n\nCompare those to the explicit forms:\n\n```python\nif len(items) > 0:\nif name == \"\" or name is None:\nif len(errors) > 0:\n```\n\nBoth are valid. The truthiness form is shorter and more idiomatic; the explicit form makes the type assumption visible. Most working Python uses the truthiness form."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "# Truthiness in action — a function that handles a possibly-empty list\ndef announce(items):\n    if items:\n        print(f\"Today's specials: {', '.join(items)}\")\n    else:\n        print(\"No specials today.\")\n\nannounce([\"soup\", \"salad\"])\nannounce([])"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## When to be explicit\n\nTruthiness collapses a lot of distinctions. `0`, `None`, `\"\"`, and `[]` all look the same to `if`. When you need to distinguish \"missing\" from \"zero\" or \"empty\", reach for an explicit `is None` check:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "def record_score(name, score=None):\n    if score:\n        print(f\"{name} scored {score}\")\n    else:\n        print(f\"No score recorded for {name}\")\n\n# This is fine for most scores...\nrecord_score(\"Ada\", 92)\n\n# ...but a score of 0 is treated as missing!\nrecord_score(\"Ben\", 0)"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "The fix is to ask the right question. We don't actually want \"is `score` truthy?\" — we want \"did the caller supply a value?\":"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "def record_score(name, score=None):\n    if score is not None:        # the right check\n        print(f\"{name} scored {score}\")\n    else:\n        print(f\"No score recorded for {name}\")\n\nrecord_score(\"Ada\", 92)\nrecord_score(\"Ben\", 0)\nrecord_score(\"Cas\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Rule of thumb:**\n\n- Use truthiness (`if items:`, `if not name:`) when \"empty\" and \"missing\" mean the same thing in this context.\n- Use `is None` / `is not None` when the difference between zero/empty and missing matters."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Customising truthiness\n\nWhen you write your own classes, you can decide what \"truthy\" means for instances by defining `__bool__`. If you don't, every instance is truthy by default."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "class Reading:\n    def __init__(self, value, valid):\n        self.value = value\n        self.valid = valid\n\n    def __bool__(self):\n        return self.valid\n\ngood = Reading(value=21.4, valid=True)\nbad = Reading(value=99.9, valid=False)\n\nif good:\n    print(f\"Got reading: {good.value}\")\nif not bad:\n    print(\"Skipped invalid reading.\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "If your class is container-like and defines `__len__`, Python uses that automatically — empty means falsy.\n\n```python\nclass Queue:\n    def __init__(self):\n        self._items = []\n    def __len__(self):\n        return len(self._items)\n\nq = Queue()\nif not q:\n    print(\"queue is empty\")  # this prints\n```\n\nFor more on this protocol, see the [Truthiness rules](https://agilearn.co.uk/guides/conditional-logic/reference/truthiness-rules) reference and the [Why truthiness works the way it does](https://agilearn.co.uk/guides/conditional-logic/concepts/why-truthiness-works-the-way-it-does) concepts essay."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Exercise: a sensible default\n\nWrite a function `greet(name=None)` that prints a friendly greeting. The rules:\n\n- If `name` is `None`, print `\"Hello, friend!\"`\n- If `name` is an empty string, also print `\"Hello, friend!\"`\n- If `name` is any other string, print `\"Hello, <name>!\"`\n\n**Hint:** Both `None` and `\"\"` are falsy, so a single truthiness check covers both cases."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "# Write your greet function here\ndef greet(name=None):\n    pass\n\n\n# Tests\ngreet()              # Expected: Hello, friend!\ngreet(\"\")            # Expected: Hello, friend!\ngreet(\"Ada\")         # Expected: Hello, Ada!"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Solution\n\nA truthiness check on `name` does the job in one branch:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "def greet(name=None):\n    if name:\n        print(f\"Hello, {name}!\")\n    else:\n        print(\"Hello, friend!\")\n\n\ngreet()\ngreet(\"\")\ngreet(\"Ada\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "You could also write it using the `or` idiom you saw earlier:\n\n```python\ndef greet(name=None):\n    print(f\"Hello, {name or 'friend'}!\")\n```\n\nBoth are idiomatic. The `or` form is shorter; the `if` form is easier to extend if you later add more rules."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Summary\n\nIn this tutorial, you learned:\n\n- How **short-circuit evaluation** lets `and` and `or` skip the right-hand side when the result is already known\n- That `and` and `or` return **one of their operands**, not necessarily `True` or `False` — and how the `value or default` idiom uses this\n- The full list of **falsy values** in Python: `False`, `None`, numeric zero, and empty containers\n- When to use **truthiness** (`if items:`) and when to be **explicit** (`if items is not None:`)\n- How to make your own classes participate in truthiness with `__bool__`\n\n## What is next\n\nThe final tutorial in this guide introduces a different approach to branching:\n\n- **[Pattern matching with `match`/`case`](https://agilearn.co.uk/guides/conditional-logic/learn/03-pattern-matching-with-match-case)** — using structural patterns when `if`/`elif` would be repetitive."
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}