An f-string is a string literal prefixed with f that embeds Python expressions inside curly braces: f"Hello, {name}". The bracket can hold any expression, optionally followed by a conversion (!r, !s, !a) and a colon-introduced format specifier from the format spec mini-language. f-strings landed in Python 3.6 (PEP 498), gained the = debug specifier in 3.8, and were re-grammared in 3.12 by PEP 701, which removed restrictions on quote characters, backslashes, comments, and nesting. They're faster than both % formatting and str.format(), but three production cases still call for the older tools: logging (lazy evaluation), SQL queries (injection safety), and internationalization (locale-aware templates). This piece is one of 17 short explainers in our Python Concepts Explained reference.

Key Takeaways

  • Syntax: f"{expression!conversion:format_spec}". Conversion and spec are both optional. The expression is any Python expression.
  • Format mini-language: [[fill]align][sign][#][0][width][grouping][.precision][type]. Each slot is independent; combine them freely.
  • Debug specifier: f"{count=}" prints count=3 with the literal name. Available since Python 3.8. Add a format spec: f"{price=:.2f}".
  • Python 3.12 changed the parser. Per PEP 701, f-strings became first-class tokens. You can now reuse the outer quote, include backslashes, nest more freely, and write multi-line expressions with comments.
  • Skip f-strings for logging, SQL, and i18n. Each has a specific reason: lazy evaluation, injection safety, locale-aware ordering.
Python f-string format spec cheat sheet Format spec mini-language cheat sheet Full format spec: {value : [[fill]align] [sign] [#] [0] [width] [,_] [.precision] [type]} 1. Alignment & Fill align: < left, > right, ^ center f"{'x':<10}|" 'x |' f"{'x':>10}|" ' x|' f"{'x':^10}|" ' x |' f"{'x':*^10}|" '****x*****|' f"{42:0>6}" '000042' fill char comes BEFORE align 2. Numbers precision, thousands, type f"{1234567.89:,}" '1,234,567.89' f"{3.14159:.2f}" '3.14' f"{0.0042:.2%}" '0.42%' f"{1234567:.2e}" '1.23e+06' f"{255:#x}" '0xff' type: f, e, %, b, o, x, X, d 3. Debug & Conversions = (3.8+), !r, !s, !a f"{count=}" 'count=3' f"{name=!r}" "name='ada'" f"{price=:.2f}" 'price=9.99' f"{name!r}" "'ada'" f"{obj!s}" 'str(obj)' !r calls repr, !s calls str, !a calls ascii 4. Dates strftime codes after the colon f"{now:%Y-%m-%d}" '2026-06-04' f"{now:%H:%M:%S}" '14:30:45' f"{now:%A}" 'Thursday' f"{now:%B %d, %Y}" 'June 04, 2026' f"{td:%H:%M}" (timedelta limited) Any object's __format__ method runs Combine slots freely: f"{price:>10,.2f}" right-aligns a 10-char field with thousands and 2 decimals.
Format spec mini-language: four common families of patterns. Each slot is independent; combine them as needed.

The Basics: f"text {expr}"

An f-string is a regular string with an f prefix. Inside the string, any text between { and } is treated as a Python expression to evaluate at runtime.
name = "Ada"
age = 36
print(f"Hello, {name}. Next year you will be {age + 1}.")
# Hello, Ada. Next year you will be 37.
The braces can hold any expression, not just a variable: arithmetic, method calls, comprehensions, ternary expressions (covered in our ternary operator explainer).
nums = [1, 2, 3, 4]
print(f"Sum: {sum(nums)}, max: {max(nums)}")
# Sum: 10, max: 4

print(f"Even? {'yes' if 4 % 2 == 0 else 'no'}")
# Even? yes
To include a literal brace, double it: {{ for a single {, }} for }. Combine with raw strings (rf"...") to skip backslash escapes; combine with the byte prefix (fb"...") is NOT allowed because the bytes type doesn't support formatting.

The Format Spec Mini-Language Decoded

The format spec sits after a colon inside the braces: f"{value:spec}". The full grammar is:
[[fill]align][sign][#][0][width][grouping][.precision][type]
Each bracketed slot is optional. The slots are positional: if you provide a fill character, it must come right before the align character, and so on. Reading the spec left-to-right tells you exactly what's happening.
SlotWhat it doesExample
fillCharacter used to pad. Any single character.*^10 centers in 10 chars padded with *
align< left, > right, ^ center, = after sign for numbers:>10
sign+ always show, - only on negative (default), space for positive:+.2f
#Alternative form: 0x for hex, 0b for binary, 0o for octal:#x
0Pad numbers with zeros instead of spaces:06d000042
widthMinimum field width:10
grouping, thousands separator, _ for underscore separator:,
.precisionDecimals for floats, max chars for strings:.2f
typef fixed, e scientific, % percent, b bin, o oct, x hex, d decimal, s string:e
The same grammar is used by str.format() and the format protocol, so learning it once pays dividends across the language. The canonical reference is the Python docs (Format Specification Mini-Language).

Number Formatting

The number-related slots are where f-strings save the most code. The common patterns:
price = 1234567.89

f"{price:.2f}"              # '1234567.89'         2 decimals
f"{price:,.2f}"             # '1,234,567.89'       thousands + 2 decimals
f"{price:+.2f}"             # '+1234567.89'        forced sign
f"{price:.2e}"              # '1.23e+06'           scientific
f"{0.0125:.1%}"             # '1.3%'               percent (multiplies by 100)

# Integers in different bases
f"{255:b}"                  # '11111111'           binary
f"{255:o}"                  # '377'                octal
f"{255:x}"                  # 'ff'                 hex (lowercase)
f"{255:X}"                  # 'FF'                 hex (uppercase)
f"{255:#x}"                 # '0xff'               with prefix

# Zero-padding
f"{42:06d}"                 # '000042'             6 chars, zero-padded
The % type multiplies the value by 100 and appends a percent sign. The e type produces scientific notation. The # flag adds the canonical prefix (0x, 0b, 0o) for non-decimal bases.

General-purpose g and locale-aware n

Two underused number types worth knowing. g picks between fixed and scientific based on magnitude, with the precision controlling total significant digits. n uses the current locale for separators and decimal mark.
f"{1234.5678:.4g}"            # '1235'           4 significant digits
f"{0.00012345:.4g}"           # '0.0001235'      switches to fixed
f"{1234567890:.4g}"           # '1.235e+09'      switches to scientific

import locale
locale.setlocale(locale.LC_ALL, "de_DE.UTF-8")
f"{1234.56:n}"                # '1.234,56'       German locale formatting
For most application code, :.2f and :, cover everything you need. Reach for g when output spans many orders of magnitude (scientific reports, telemetry); reach for n for user-facing numbers in locale-aware UIs.Python code on a dark editor screen showing f-string formatting with format specifiers

Alignment, Padding, and Fill

Width and alignment go together. Specify the width as a number; specify the alignment with one of <, >, ^; optionally specify a fill character before the alignment.
f"{'x':<10}|"       # 'x         |'      left, width 10
f"{'x':>10}|"       # '         x|'      right, width 10
f"{'x':^10}|"       # '    x     |'      center, width 10
f"{'x':*^10}|"      # '****x*****|'      center with * fill
f"{'x':->10}|"      # '---------x|'      right with - fill
The = alignment is special and applies only to numbers: it places the fill characters between the sign and the digits, which matters for column-aligned tables of signed numbers.
f"{-42:=+6}"        # '-   42'           sign at left, padding inside
f"{ 42:=+6}"        # '+   42'
For tabular output, combine alignment with grouping and precision into one spec: f"{value:>12,.2f}" right-aligns in a 12-character field with thousands separators and two decimal places. Pages of code shrink to one line.

The Debug Specifier = (Python 3.8+)

Added in Python 3.8, the = suffix is the single most quoted feature of modern f-strings. It expands to the literal expression text, an equals sign, and the repr of the value.
count = 3
name = "Ada"
price = 9.99

f"{count=}"             # 'count=3'
f"{name=}"              # "name='Ada'"        ← uses repr by default
f"{count + 5=}"         # 'count + 5=8'       expressions work
It composes with the conversion flags and the format spec.
f"{name=!s}"            # 'name=Ada'           force str instead of repr
f"{price=:.2f}"         # 'price=9.99'         format spec applied
f"{name=!r:>20}"        # right-aligned repr after the equals
The original use case was debug printing. Instead of typing print(f"count = {count}") every time you want to inspect a value, type print(f"{count=}"). The four extra characters save you typing the variable name twice and prevent the "I changed the name and forgot to update the label" bug.

Conversions: !r, !s, !a

Conversion flags sit between the expression and the format spec. They call one of Python's built-in conversion functions on the value before formatting.
name = "Ada"

f"{name!r}"     # "'Ada'"           ← repr() — quoted, shows type
f"{name!s}"     # 'Ada'             ← str() — same as default
f"{name!a}"     # "'Ada'"           ← ascii() — non-ASCII escaped
The most common use is !r inside log messages or error messages where you want the user to see exactly what value was passed, including quote marks for strings. str.format() uses the same conversions, so the syntax transfers.

Dates and Custom Objects

Anything inside the braces is just an expression; what comes after the colon is the format spec. If the value has a __format__ method, that method receives the spec as a string and returns the formatted output. For dates, datetime.__format__ interprets the spec as a strftime format string.
from datetime import datetime

now = datetime(2026, 6, 4, 14, 30, 45)

f"{now:%Y-%m-%d}"             # '2026-06-04'
f"{now:%Y-%m-%d %H:%M:%S}"    # '2026-06-04 14:30:45'
f"{now:%A, %B %d}"            # 'Thursday, June 04'
f"{now:%I:%M %p}"             # '02:30 PM'
The strftime codes are documented in the standard library; %Y for year, %m for month, %d for day, and so on. timedelta doesn't have a built-in format protocol; if you need formatted durations, compute the parts manually or use str(td).

PEP 701: What Python 3.12 Actually Changed

Before 3.12, f-strings were special tokens with restrictions. You couldn't reuse the outer quote character inside the expression, couldn't include backslashes, couldn't write multi-line expressions, couldn't include comments. The workarounds were ugly: alternate quote characters, separate temporary variables, awkward formatting.PEP 701 rewrote the f-string grammar so f-strings are first-class tokens, parsed exactly like the surrounding Python code. The user-visible changes:

Reusing the outer quote

data = {"name": "Ada"}

# Python 3.11: needs alternate quotes
f'{data["name"]}'

# Python 3.12+: can reuse the outer "
f"{data["name"]}"

Backslashes

# Python 3.11: NOT allowed
# f"{some_string.replace('\n', ' ')}"   ← SyntaxError

# Python 3.12+: allowed
f"{some_string.replace('\n', ' ')}"

Multi-line expressions and inline comments

# Python 3.12+
f"{
    sum(   # the total
        value
        for value in items
        if value > 0
    )
}"

Deeper nesting

Quote conflicts that used to require escaping or temporary variables now resolve cleanly because the parser knows which level of nesting it's at. f-strings can nest f-strings inside their own expressions without limit.
Version note: the grammar change ships only in Python 3.12 and later. If your code must support 3.11 or earlier, write the conservative form: alternate quotes, no backslashes, single-line expressions.

Multi-line f-strings and Escape Characters

f-strings respect newlines exactly like regular strings: a triple-quoted f-string spans lines and preserves them in the output, while a single-quoted f-string is limited to one source line (though it can hold large expressions).
name = "Ada"
n_items = 5

# Triple-quoted multi-line
message = f"""
Hello, {name}.
You have {n_items} items in your cart.
Total: ${5 * 12.99:.2f}
"""

# Single-quoted, one line in source
note = f"User {name} bought {n_items} items totaling ${5 * 12.99:.2f}"

Embedded escape characters

The braces interpret escape sequences in the format spec exactly like regular strings. A tab character in a fill slot needs \t; the same for newlines, backslashes, and Unicode escapes.
print(f"line 1{chr(10)}line 2")        # explicit newline via chr
print(f"a\tb\tc")                       # tabs in plain text
print(f"unicode: {0x1F600:c}")          # 😀 from code point (note c type for char)

# Python 3.12+: backslashes allowed in expressions
print(f"path: {'a\\nb'.replace(chr(92)+'n', '/')}")
For raw text where you don't want escape interpretation, combine r and f prefixes: rf"raw\n with {value}". The order doesn't matter; fr and rf are equivalent.

Reading f-string Error Messages

Before Python 3.12, f-string parsing errors often surfaced as confusing SyntaxError messages pointing at the wrong line. PEP 701 dramatically improved the diagnostics. A few common errors and their interpretation:
f"{x"                  # SyntaxError: f-string: expecting '}'
f"{}"                  # SyntaxError: f-string: empty expression not allowed
f"{x!q}"               # SyntaxError: f-string: invalid conversion character: expected 's', 'r', or 'a'
f"{x:.2y}"             # ValueError: Unknown format code 'y' for object of type 'int'   (at runtime)
The first three are syntax errors at compile time. The fourth is a runtime error because the format spec is only validated when the value's __format__ method runs. The version-specific behavior matters: 3.11's parser was permissive about some malformed specs that 3.12 catches at compile time. When porting code, run the test suite on the newer Python; new errors usually indicate latent bugs.

Custom __format__ for Your Own Classes

The format spec inside the colon is just a string passed to the value's __format__ method. If you write a class that defines __format__, your class controls the spec interpretation.
class Money:
    def __init__(self, amount, currency="USD"):
        self.amount = amount
        self.currency = currency

    def __format__(self, spec):
        if spec == "":
            return f"{self.amount:.2f} {self.currency}"
        if spec == "short":
            return f"{self.amount:.0f}"
        if spec == "code":
            return f"{self.currency}{self.amount:.2f}"
        # Pass through standard numeric specs to the underlying float
        return f"{self.amount:{spec}} {self.currency}"

m = Money(1234.5)
print(f"{m}")              # '1234.50 USD'
print(f"{m:short}")        # '1234'
print(f"{m:code}")         # 'USD1234.50'
print(f"{m:,.2f}")         # '1,234.50 USD'   ← passed through
The pattern is powerful for domain objects: dates, currencies, distances, ratings, anything you want to format in domain-specific ways. The __format__ method receives the bare spec string (no colon) and returns a string. If your class wraps a numeric type, the cleanest design is to pass numeric specs through to the underlying value and only intercept your custom ones.

Migrating From % and str.format()

Most Python codebases written before 3.6 used either C-style % formatting or the str.format() method. Both still work, both are still valid Python, and both can be mechanically converted to f-strings in almost every case.

From % formatting

# Old
"Hello, %s. You owe $%.2f." % (name, amount)

# New
f"Hello, {name}. You owe ${amount:.2f}."
The mapping is one-to-one: %s becomes {var}, %d becomes {var} (Python figures out the type), %.2f becomes {var:.2f}, %x becomes {var:x}. The named form "%(name)s" % {"name": value} becomes f"{value}" at the call site.

From str.format()

# Old
"Hello, {name}. You owe ${amount:.2f}.".format(name=name, amount=amount)

# New
f"Hello, {name}. You owe ${amount:.2f}."
The format spec inside the braces is identical; only the prefix and the explicit .format(...) call go away. The tool flynt (pip install flynt) performs this conversion mechanically across a codebase. For projects targeting Python 3.6 or newer, the migration is essentially risk-free.

When the migration is wrong

The three exception cases below (logging, SQL, i18n) are the same code paths where % formatting was the right tool before f-strings existed, and they're still the right tool now. Don't convert those.

When NOT to Use f-strings

Three production cases where f-strings are the wrong choice.

Logging: use the % template form

import logging
log = logging.getLogger(__name__)

# Don't do this:
log.debug(f"user {user.name} did {action} on {expensive_lookup()}")

# Do this:
log.debug("user %s did %s on %s", user.name, action, expensive_lookup())
The f-string version evaluates every expression before the call, including expensive_lookup(), even when the debug level is disabled. The %-template form delays formatting until the logger decides the message is actually going to be emitted. For high-volume logs at DEBUG level disabled, the cost difference is measurable.

SQL queries: use parameterized queries

# Don't do this — SQL injection waiting to happen:
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")

# Do this:
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
f-string interpolation into SQL is a textbook injection vulnerability. The placeholder form ships the SQL and the parameters separately; the database driver handles escaping. This rule applies to every SQL dialect and every Python driver.

Internationalization: use a template library

f-strings hardcode variable ordering and English grammar at the source. For multi-language output, use gettext, babel, or another i18n framework that supports translated templates where translators can reorder variables and choose locale-appropriate forms (plurals, dates, numbers).

Performance: f-strings Are the Fastest

For the same output, f-strings are faster than both % formatting and str.format(). Approximate timings for "hello, " + name + ", you are " + str(age) equivalents (CPython 3.12, microbenchmark):
MethodTime per callNotes
f-string~120 nsbaseline
str.format~280 ns~2.3× slower
% formatting~190 ns~1.6× slower
String concatenation~140 nsfast but error-prone
"".join(...) with map(str)~250 nsslower than concatenation for small N
The speed difference is invisible for typical application code (you'd need to format millions of strings per second to notice). For hot inner loops in performance-critical paths, f-strings remain the best choice.

The Future: t-strings in Python 3.14

Python 3.14 introduces template strings via PEP 750: literals prefixed with t that capture the structure of an interpolated string without immediately rendering it. The result is a Template object that downstream code can examine, transform, or render with custom logic.
# Python 3.14+ syntax (preview)
template = t"Hello, {name}, you owe ${amount:.2f}"
print(type(template))   # <class 'string.templatelib.Template'>

# Downstream code can inspect the parts:
for part in template:
    if isinstance(part, str):
        print("literal:", part)
    else:
        print("interpolation:", part.expression, part.value, part.format_spec)
The use case: safe SQL builders, HTML-escaping templates, structured loggers, and other tools that need to know which parts came from user input and which are literal. Today's workarounds (parameterized queries, manual escaping, lazy logging) become uniform under t-strings: the framework receives the structure and decides how to render it.t-strings don't replace f-strings; they complement them. f-strings remain the right tool for "render this string now"; t-strings target "capture the structure for later". The pattern matures over the next few Python releases as libraries adopt it.

Real-World Patterns

Three patterns that show up constantly in production code.

Tabular output

users = [("alice", 95, 30), ("bob", 80, 25), ("charlie", 70, 35)]

print(f"{'Name':<10} {'Score':>6} {'Age':>4}")
print("-" * 22)
for name, score, age in users:
    print(f"{name:<10} {score:>6} {age:>4}")

# Name        Score  Age
# ----------------------
# alice          95   30
# bob            80   25
# charlie        70   35

Progress reports

for i, item in enumerate(items, start=1):
    pct = i / len(items)
    print(f"\r[{i:>4}/{len(items):<4}] {pct:>6.1%}", end="")

Aligned key-value pairs

config = {
    "host": "localhost",
    "port": 8080,
    "timeout": 30.5,
    "debug": True,
}

width = max(len(k) for k in config) + 2
for key, value in config.items():
    print(f"  {key+':':<{width}} {value!r}")

#   host:     'localhost'
#   port:     8080
#   timeout:  30.5
#   debug:    True
The format spec can itself contain expressions: {key+':':<{width}} uses width (computed above) as the field width. Nesting one f-string inside another's spec is legal and common in tabular output where the width is data-dependent.

JSON-like inline summaries

def describe(event):
    return (
        f'{{"type": {event.type!r}, '
        f'"user": {event.user!r}, '
        f'"timestamp": {event.timestamp.isoformat()!r}, '
        f'"duration_ms": {event.duration * 1000:.0f}}}'
    )

print(describe(event))
# {"type": "click", "user": "ada", "timestamp": "2026-06-04T14:30:45", "duration_ms": 230}
For real JSON output use json.dumps (which handles edge cases properly); the inline form above is useful for debug logs and ad-hoc terminal output where readability beats strict correctness.

Structured summary lines

elapsed = 2.3456
n_rows = 1234567
errors = 3

summary = f"Processed {n_rows:,} rows in {elapsed:.2f}s ({errors} errors)"
# 'Processed 1,234,567 rows in 2.35s (3 errors)'
The f-string version is shorter and reads top-to-bottom in the same order as the output, which makes it easier to keep in sync with whatever business logic feeds the values.

f-string Cookbook: Six Patterns Worth Memorizing

The patterns that come up so often they're worth committing to memory.

1. Currency with two decimals and thousands

amount = 1234567.89
f"${amount:,.2f}"   # '$1,234,567.89'

2. Percentage with one decimal

ratio = 0.0125
f"{ratio:.1%}"   # '1.3%'   (multiplies by 100 automatically)

3. Padded ID (e.g. order numbers)

order_id = 42
f"ORD-{order_id:06d}"   # 'ORD-000042'

4. Right-aligned column for a number

for n in [1, 10, 100, 1000]:
    print(f"{n:>8} items")
#        1 items
#       10 items
#      100 items
#     1000 items

5. ISO 8601 timestamp

from datetime import datetime
now = datetime.utcnow()
f"{now:%Y-%m-%dT%H:%M:%SZ}"   # '2026-06-04T14:30:45Z'

6. Debug a few variables on one line

x, y = 3, "hello"
print(f"{x=} {y=}")   # "x=3 y='hello'"
Memorize these six and you'll cover 80% of real-world f-string formatting. The remaining 20% comes from combining the slots; the cheat sheet at the top of this article shows every combination worth seeing.

Common Mistakes

Five traps to watch for: Mistake 1: forgetting the f prefix. Without the prefix, the braces are literal text. "Hello, {name}" prints exactly that. The error is silent until you read the output. Mistake 2: using f-strings in logging calls. Covered above. Use the % template form so disabled log levels don't pay the formatting cost. Mistake 3: SQL injection via f-strings. Also covered above. Always use parameterized queries. Mistake 4: the = debug specifier in user-facing strings. f"{x=}" is for debugging, not for output. Production strings should use plain f"x: {x}". Mistake 5: backslashes in expressions on Python 3.11 and earlier. The pre-3.12 parser rejects f-strings whose expressions contain backslashes. Pull the value into a temporary variable, or use a different quote style. Don't fight the parser.

Frequently Asked Questions

What is an f-string in Python?

An f-string is a string literal prefixed with f or F that allows you to embed Python expressions inside curly braces. The expressions are evaluated at runtime and converted to strings, and an optional format specifier after a colon controls how the result is formatted. f-strings were introduced in Python 3.6 via PEP 498. They're faster than the older % and str.format() forms and read more naturally because the values appear inline with the surrounding text.

How do I format a number with f-strings?

Add a format specifier after a colon: f"{price:.2f}" for two decimals, f"{n:,}" for thousands separators, f"{x:.2%}" for percentages, f"{x:.2e}" for scientific notation, f"{n:b}" for binary, f"{n:x}" for hexadecimal. Combine fields: f"{price:>10,.2f}" right-aligns a 10-character field with comma separators and two decimals. The full format spec mini-language documents every option.

What does the = inside an f-string do?

Added in Python 3.8, the = suffix is the debug specifier. f"{x=}" expands to the literal text x= followed by the repr of x, so f"{count=}" prints count=3 if count is 3. You can combine = with conversions and format specs: f"{count=!r}" or f"{price=:.2f}". It's the cleanest tool for ad-hoc debug printing and replaces dozens of print(f"x = {x}") lines in older code.

What changed in Python 3.12 f-strings?

PEP 701 made f-strings first-class tokens in the parser. The practical effects: you can reuse the outer quote character inside the expression (f"{d['key']}"), include backslashes, write multi-line expressions including comments, and nest f-strings deeper without quote conflicts. Error messages also became more specific.

When should I not use f-strings in Python?

Three cases. First, logging: logger.info(f"user {name} did {action}") evaluates the f-string even when the log level is disabled. Use logger.info("user %s did %s", name, action) so the formatting only happens if the message will actually be emitted. Second, SQL queries: f-string interpolation is SQL injection waiting to happen. Use parameterized queries with cursor.execute(sql, params). Third, internationalization: f-strings hardcode the English ordering of variables in the source; gettext and locale-aware tools need a more flexible template.

The Bottom Line: Embed, Format, Ship

f-strings are the canonical Python way to interpolate values into strings: faster than the alternatives, more readable than concatenation, and powerful enough to handle every formatting case via the mini-language. Memorize the format spec slots, lean on the = debug specifier for inspection, and remember that custom classes can implement __format__ for domain-specific formatting. The three places to skip them, logging, SQL, and i18n, have specific reasons rooted in lazy evaluation, security, and locale flexibility. With those three exceptions in mind, every other "format a string in Python" question is an f-string. For the rest of the most-asked Python concept questions, browse the full Python Concepts Explained index.

Drill f-string Patterns on Real Tasks

CodeGym's Python track turns string formatting into muscle memory through 800+ hands-on tasks across 62 levels. The AI validator checks every submission in seconds; the AI mentor explains what broke when you get stuck. First level free; full plan on the pricing page. Begin your Python learning path →