Slicing is how you pull a piece out of a string by position. The syntax s[start:stop:step] covers everything from "the first three characters" to "every other character, in reverse". This lesson walks through the slice syntax in detail: the half-open interval, what each default does, how negative bounds and a negative step behave, why slicing past the end is safe, and what each slice costs in memory.
s[start:stop]A slice carves out a substring by giving Python two indices. The first character of the result is at start. The slice stops just before stop, never at it. That's called a half-open interval, and once you internalize it, every other slicing rule clicks into place.
product[0:4] gives positions 0, 1, 2, 3, which spells "wire". Position 4 is excluded. product[2:6] covers positions 2, 3, 4, 5, which spells "rele". product[4:8] runs from position 4 up to but not including position 8, which is "less".
The half-open rule has a convenient side effect: the length of the slice is always stop - start (when both are non-negative and inside the string). product[2:6] is 6 - 2 = 4 characters long. You don't have to count off-by-one.
Here's the index model for "laptop". The top row shows the standard positive indices, and the bottom row shows the negative indices Python also accepts:
laptop[1:4] starts at index 1 (a) and stops just before index 4 (o), so it returns "apt". Three characters, indices 1, 2, 3.
Cost: Every slice creates a new string object. The cost is O(k) in time and memory, where k is the length of the slice. Slicing a million-character string to keep three characters is fine. Slicing a million-character string in a loop to build something larger is not.
start, stop, or BothEach piece of a slice has a default. Leave out start and Python uses 0. Leave out stop and Python uses the length of the string (which means "go all the way to the end"). Leave out both and you get the whole string.
category[:5] is the same as category[0:5]. It's the first five characters: "elect". category[5:] is the same as category[5:len(category)]. It's the rest of the string starting at position 5: "ronics". category[:] covers the entire string.
These two patterns, "first N characters" and "everything from position N onward", show up constantly. Splitting a string at a known cut point is so common that the [:N] and [N:] shorthand is one of the first things to memorize:
The [:] form is special. It returns the whole string, but unlike for lists or other mutable sequences, slicing a string with [:] doesn't make a copy. Python returns the same string object because strings are immutable, so there's no need to allocate a new one. For now just know that s[:] is s for strings:
The is operator checks whether two names point to the exact same object in memory. Here it returns True, which proves that [:] on a string returns the same object, not a copy.
Cost: s[:] on a string is O(1) and allocates nothing. It's a special case because strings are immutable. For lists, lst[:] is the standard idiom for making a shallow copy and does cost O(n).
Negative indices are useful when accessing single characters too: s[-1] is the last character, s[-2] is the second-to-last, and so on. The same idea works inside a slice. Negative numbers count from the end, with -1 being the last character.
product[-5:] says "start five characters from the end and go to the end": "phone". product[:-3] says "start at the beginning and stop three characters before the end": "smartph". product[-7:-2] runs from position -7 (a) up to but not including position -2 (n), which gives "artpho".
You can mix positive and negative bounds freely. Python converts negatives to positives internally by adding the length, then applies the same half-open rule:
Both slices land on the same range. The first uses positive 6 and negative -1. The second uses negative -7 and positive 12. Python doesn't care about the form; only the resolved positions matter.
Here's a small table of the most common "give me the last N" and "drop the last N" patterns:
| Slice | Meaning | Example on "smartphone" |
|---|---|---|
s[-1] | Last character | "e" |
s[-N:] | Last N characters | s[-5:] is "phone" |
s[:-N] | Everything except the last N | s[:-5] is "smart" |
s[-N:-M] | From N-from-end up to M-from-end | s[-7:-2] is "artpho" |
The "drop the last N" pattern is handy when you know a string has a trailing piece you want to lose. Trimming a .csv extension from a filename is filename[:-4]. Removing a single trailing character is s[:-1].
stepThe full slice syntax is s[start:stop:step]. The step controls how far Python jumps between characters. The default step is 1, which is why s[a:b] walks character by character. Set step to 2 and Python takes every second character. Set it to 3 and you get every third.
product_code[::2] starts at index 0, stops at the end, and jumps two positions each time: A, C, E, G, I. product_code[::3] jumps three positions: A, D, G, J. product_code[1::2] starts at index 1 and takes every other character from there: B, D, F, H, J. product_code[0:8:2] does the same jumping but stops before index 8.
The step value is the most useful for sampling and reversing, less so for everyday substring extraction. You'll see it most often as [::2] (every other character) and [::-1] (reverse, covered next).
Here every other character of an alternating log gives just the entries at even positions.
Set step to a negative number and Python walks the string backward. The most common form, s[::-1], reads "from the end of the string to the start, taking every character in reverse". It's the standard way to reverse a string in Python.
Negative steps invert the meaning of start and stop. With a positive step, the slice goes from start upward toward stop. With a negative step, the slice goes from start downward toward stop. The half-open rule still holds: the character at stop is not included.
product[::-1] reverses the whole string. product[::-2] reverses and skips every other character. product[8:2:-1] starts at index 8 and walks down toward (but not including) index 2: positions 8, 7, 6, 5, 4, 3, which is "nohpda". product[-1:-5:-1] starts at the last character and walks down toward (but not including) the fifth-from-last: that's "seno".
The defaults flip too. When step is negative, an omitted start defaults to the last index (not 0), and an omitted stop defaults to before the first index (not the length). That's what makes s[::-1] work without spelling out the endpoints.
Both produce the same reversed string. The first relies on the implicit defaults for negative step. The second writes them out.
Cost: Reversing with [::-1] is O(n) in both time and memory because it allocates a brand-new string. For one-off reversals this is fine. If you only need to iterate over a string in reverse, for ch in reversed(s): walks the original without allocating a copy.
Indexing a single character past the end raises IndexError. Slicing past the end does not. Python silently clips any out-of-range bound to the string's actual length, so a slice always returns a valid (possibly empty) string.
product[2:100] clips 100 down to len(product), which is 5, so it returns product[2:5], which is "one". product[100:200] clips both bounds: start becomes 5 (the length), stop becomes 5 too, and an empty slice comes back. product[-100:3] clips the negative start down to 0, giving product[0:3], which is "pho". product[:1000] clips stop to 5, returning the whole string.
Compare this with single-character indexing, which is strict:
The single-character access raises. The slice with the same out-of-range value just returns an empty string. This asymmetry is intentional. Single indexing asks for one specific character; if it doesn't exist, that's an error. Slicing asks for a range; an empty range is a valid answer.
This safety is what lets you write code like s[:5] without checking whether s has at least five characters first. If it has three, you get all three back, no exception.
The same [:5] slice handles both names cleanly: it returns the entire short name, and the first five characters of the long one. No length check needed.
A handful of slicing patterns come up often enough in real code that they're worth memorizing as recipes. Each one is a small idiom built from the rules above.
The "last N characters" pattern uses a negative start and an empty stop:
This is the right tool for showing the tail of a tracking number, the last few digits of an order ID, or any "give me the suffix" task.
Two of the most common one-character trims:
s[1:] drops the first character. s[:-1] drops the last. They compose: s[1:-1] drops both, which is handy for stripping matching brackets or quotes:
A common privacy pattern: hide everything except the last few characters of a sensitive number. Slicing makes this a one-liner.
The slice card_number[-4:] grabs the last four digits. The rest of the string gets replaced with asterisks, padded to the original length using string repetition. This is the standard recipe for showing a partial identifier in a UI without leaking the full value.
Sometimes you need a name in reverse, for a sort key, a hash bucket, or a quick comparison:
[::-1] works on any string, including names with spaces and punctuation. The slice walks character by character from end to start.
The skip-step idiom is useful when sampling from a fixed-pattern string:
When characters at even and odd positions carry different meaning, two slices with step=2 separate them in one line each.
Strings are immutable, so every slice that returns a different string must allocate a brand-new object. There's no way to "view" part of a string without copying its characters. The cost of slicing is proportional to the length of the result, not the length of the original string.
The first slice copies 10 characters. The second copies 60. The third, [:], returns the same object because it covers the whole string and strings are immutable, so there's no need to allocate.
This adds up in loops. Building a result by repeatedly slicing and concatenating is a classic O(n²) trap. Each round produces a new string the size of the running total, then throws away the previous one:
For five items this is fine. For five thousand it's slow, because each result + part + ", " allocates a new string holding everything seen so far. The Pythonic alternative for this kind of work is ", ".join(parts). The point here is that slices and concatenations both allocate, so combining them in a tight loop multiplies the cost.
Cost: A slice of length k costs O(k) time and O(k) memory. The s[:] shortcut is the only zero-cost slice, and only because strings are immutable. Don't reach for slicing inside a hot loop just to "look at" part of a string; index a single character with s[i] instead, since that doesn't allocate.
For a single use, even repeated slicing of a long string is fine. The cost only matters when you're slicing in a loop or building results piece by piece.
Three slices, three small allocations, and the original review string is unchanged. That's the typical shape of slicing in real code: a few targeted extractions, not a million in a loop.
10 quizzes