What the f*ck Python! 🐍
An interesting collection of surprising snippets and lesser-known Python features.
[![WTFPL 2.0][license-image]][license-url]
Translations: Chinese 中文
Python, being a beautifully designed high-level and interpreter-based programming language, provides us with many features for the programmer's comfort. But sometimes, the outcomes of a Python snippet may not seem obvious to a regular user at first sight.
Here is a fun project to collect such tricky & counter-intuitive examples and lesser-known features in Python, attempting to discuss what exactly is happening under the hood!
While some of the examples you see below may not be WTFs in the truest sense, but they'll reveal some of the interesting parts of Python that you might be unaware of. I find it a nice way to learn the internals of a programming language, and I believe that you'll find it interesting too!
If you're an experienced Python programmer, you can take it as a challenge to get most of them right in first attempt. You may be already familiar with some of these examples, and I might be able to revive sweet old memories of yours being bitten by these gotchas 😅
PS: If you're a returning reader, you can learn about the new modifications here.
So, here we go...
Table of Contents
- Structure of the Examples
- Usage
- 👀 Examples
- Section: Strain your brain!
- ▶ Strings can be tricky sometimes *
- ▶ Splitsies ^
- ▶ Time for some hash brownies!
- ▶ The disorder within order ^
- ▶ Keep trying? *
- ▶ Deep down, we're all the same. *
- ▶ For what?
- ▶ Evaluation time discrepancy ^
- ▶ Messing around with
is
operator^ - ▶ A tic-tac-toe where X wins in the first attempt!
- ▶ The sticky output function
- ▶ The chicken-egg problem ^
- ▶
is not ...
is notis (not ...)
- ▶ The surprising comma
- ▶ Strings and the backslashes\ ^
- ▶ not knot!
- ▶ Half triple-quoted strings
- ▶ Midnight time doesn't exist?
- ▶ What's wrong with booleans?
- ▶ Class attributes and instance attributes
- ▶ yielding None
- ▶ Mutating the immutable!
- ▶ The disappearing variable from outer scope
- ▶ When True is actually False
- ▶ Lossy zip of iterators
- ▶ From filled to None in one instruction...
- ▶ Subclass relationships *
- ▶ The mysterious key type conversion *
- ▶ Let's see if you can guess this?
- Section: Appearances are deceptive!
- Section: Watch out for the landmines!
- ▶ Modifying a dictionary while iterating over it
- ▶ Stubborn
del
operation * - ▶ Deleting a list item while iterating
- ▶ Loop variables leaking out!
- ▶ Beware of default mutable arguments!
- ▶ Catching the Exceptions
- ▶ Same operands, different story!
- ▶ The out of scope variable
- ▶ Be careful with chained operations
- ▶ Name resolution ignoring class scope
- ▶ Needles in a Haystack ^
- ▶ Yielding from... return!
- ▶ Wild imports
- Section: The Hidden treasures!
- Section: Miscellaneous
- Section: Strain your brain!
- Contributing
- Acknowledgements - Some nice Links!
- 🎓 License
Structure of the Examples
All the examples are structured like below:
▶ Some fancy Title *
The asterisk at the end of the title indicates the example was not present in the first release and has been recently added.
# Setting up the code. # Preparation for the magic...
Output (Python version(s)):
>>> triggering_statement Some unexpected output
(Optional): One line describing the unexpected output.
💡 Explanation:
- Brief explanation of what's happening and why is it happening.
Output Output (Python version(s)):Setting up examples for clarification (if necessary)
>>> trigger # some example that makes it easy to unveil the magic # some justified output
Note: All the examples are tested on Python 3.5.2 interactive interpreter, and they should work for all the Python versions unless explicitly specified in the description.
Usage
A nice way to get the most out of these examples, in my opinion, will be to just read them chronologically, and for every example:
- Carefully read the initial code for setting up the example. If you're an experienced Python programmer, most of the times you will successfully anticipate what's going to happen next.
- Read the output snippets and,
- Check if the outputs are the same as you'd expect.
- Make sure if you know the exact reason behind the output being the way it is.
- If the answer is no (which is perfectly okay), take a deep breath, and read the explanation (and if you still don't understand, shout out! and create an issue here).
- If yes, give a gentle pat on your back, and you may skip to the next example.
PS: You can also read WTFPython at the command line. There's a pypi package and an npm package (which supports colored formatting) for the same.
To install the npm package wtfpython
$ npm install -g wtfpython
Alternatively, to install the pypi package wtfpython
$ pip install wtfpython -U
Now, just run wtfpython
at the command line which will open this collection in your selected $PAGER
.
👀 Examples
Section: Strain your brain!
▶ Strings can be tricky sometimes *
1\. ```py >>> a = "some_string" >>> id(a) 140420665652016 >>> id("some" + "_" + "string") # Notice that both the ids are same. 140420665652016 ``` 2\. ```py >>> a = "wtf" >>> b = "wtf" >>> a is b True >>> a = "wtf!" >>> b = "wtf!" >>> a is b False ``` 3\. **Output (< Python3.7 )** ```py >>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa' True >>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa' False ``` Makes sense, right? #### 💡 Explanation: + Such behavior is due to CPython optimization (called string interning) that tries to use existing immutable objects in some cases rather than creating a new object every time. + After being interned, many variables may point to the same string object in memory (thereby saving memory). + In the snippets above, strings are implicitly interned. The decision of when to implicitly intern a string is implementation dependent. There are some facts that can be used to guess if a string will be interned or not: * All length 0 and length 1 strings are interned. * Strings are interned at compile time (`'wtf'` will be interned but `''.join(['w', 't', 'f']` will not be interned) * Strings that are not composed of ASCII letters, digits or underscores, are not interned. This explains why `'wtf!'` was not interned due to `!`. Cpython implementation of this rule can be found [here](https://github.com/python/cpython/blob/3.6/Objects/codeobject.c#L19)![](/docs/wtfpython/media/commit/45638fa6b1cde184dba3acf0da97d56d2147b0c0/images/string-intern/string_intern.png)
025eb98dc0/Python/future.c (L49)
) in `future.c` file.
+ When the CPython compiler encounters a [future statement](https://docs.python.org/3.3/reference/simple_stmts.html#future-statements), it first runs the appropriate code in `future.c` before treating it as a normal import statement.
---
### ▶ Let's meet Friendly Language Uncle For Life ^
**Output (Python 3.x)**
```py
>>> from __future__ import barry_as_FLUFL
>>> "Ruby" != "Python" # there's no doubt about it
File "some_file.py", line 1
"Ruby" != "Python"
^
SyntaxError: invalid syntax
>>> "Ruby" <> "Python"
True
```
There we go.
#### 💡 Explanation:
- This is relevant to [PEP-401](https://www.python.org/dev/peps/pep-0401/) released on April 1, 2009 (now you know, what it means).
- Quoting from the PEP-401
> Recognized that the != inequality operator in Python 3.0 was a horrible, finger pain inducing mistake, the FLUFL reinstates the <> diamond operator as the sole spelling.
- There were more things that Uncle Barry had to share in the PEP; you can read them [here](https://www.python.org/dev/peps/pep-0401/).
- It works well on interactive environment, but it will raise a `SyntaxError` when you run via python file (see this [issue](https://github.com/satwikkansal/wtfpython/issues/94)). However, you can wrap the statement inside an `eval` or `compile` to get it working,
```py
from __future__ import barry_as_FLUFL
print(eval('"Ruby" <> "Python"'))
```
---
### ▶ Even Python understands that love is complicated *
```py
import this
```
Wait, what's **this**? `this` is love ❤️
**Output:**
```
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
```
It's the Zen of Python!
```py
>>> love = this
>>> this is love
True
>>> love is True
False
>>> love is False
False
>>> love is not True or False
True
>>> love is not True or False; love is love # Love is complicated
True
```
#### 💡 Explanation:
* `this` module in Python is an easter egg for The Zen Of Python ([PEP 20](https://www.python.org/dev/peps/pep-0020)).
* And if you think that's already interesting enough, check out the implementation of [this.py](https://hg.python.org/cpython/file/c3896275c0f6/Lib/this.py). Interestingly, the code for the Zen violates itself (and that's probably the only place where this happens).
* Regarding the statement `love is not True or False; love is love`, ironic but it's self-explanatory.
---
### ▶ Yes, it exists!
**The `else` clause for loops.** One typical example might be:
```py
def does_exists_num(l, to_find):
for num in l:
if num == to_find:
print("Exists!")
break
else:
print("Does not exist")
```
**Output:**
```py
>>> some_list = [1, 2, 3, 4, 5]
>>> does_exists_num(some_list, 4)
Exists!
>>> does_exists_num(some_list, -1)
Does not exist
```
**The `else` clause in exception handling.** An example,
```py
try:
pass
except:
print("Exception occurred!!!")
else:
print("Try block executed successfully...")
```
**Output:**
```py
Try block executed successfully...
```
#### 💡 Explanation:
- The `else` clause after a loop is executed only when there's no explicit `break` after all the iterations.
- `else` clause after try block is also called "completion clause" as reaching the `else` clause in a `try` statement means that the try block actually completed successfully.
---
### ▶ Ellipsis ^
```py
def some_func():
Ellipsis
```
**Output**
```py
>>> some_func()
# No output, No Error
>>> SomeRandomString
Traceback (most recent call last):
File "", line 1, in
NameError: name 'SomeRandomString' is not defined
>>> Ellipsis
Ellipsis
```
#### 💡 Explanation
- In Python, `Ellipsis` is a globally available builtin object which is equivalent to `...`.
```py
>>> ...
Ellipsis
```
- Eliipsis can be used for several purposes,
+ As a placeholder for code that hasn't been written yet (just like `pass` statement)
+ In slicing syntax to represent the full slices in remaining direction
```py
>>> import numpy as np
>>> three_dimensional_array = np.arange(8).reshape(2, 2, 2)
array([
[
[0, 1],
[2, 3]
],
[
[4, 5],
[6, 7]
]
])
```
So our `three_dimensional_array` is an array of array of arrays. Let's say we want to print the second element (index `1`) of all the innermost arrays, we can use Ellipsis to bypass all the preceding dimensions
```py
>>> three_dimensional_array[:,:,1]
array([[1, 3],
[5, 7]])
>>> three_dimensional_array[..., 1] # using Ellipsis.
array([[1, 3],
[5, 7]])
```
Note: this will work for any number of dimensions. You can even select slice in first and last dimension and ignore the middle ones this way (`n_dimensional_array[firs_dim_slice, ..., last_dim_slice]`)
+ In [type hinting](https://docs.python.org/3/library/typing.html) to indicate only a part of the type (like `(Callable[..., int]` or `Tuple[str, ...]`))
+ You may also use Ellipsis as a default function argument (in the cases when you want to differentiate between the "no argument passed" and "None value passed" scenarios).
---
### ▶ Inpinity *
The spelling is intended. Please, don't submit a patch for this.
**Output (Python 3.x):**
```py
>>> infinity = float('infinity')
>>> hash(infinity)
314159
>>> hash(float('-inf'))
-314159
```
#### 💡 Explanation:
- Hash of infinity is 10⁵ x π.
- Interestingly, the hash of `float('-inf')` is "-10⁵ x π" in Python 3, whereas "-10⁵ x e" in Python 2.
---
### ▶ Let's mangle ^
1\.
```py
class Yo(object):
def __init__(self):
self.__honey = True
self.bro = True
```
**Output:**
```py
>>> Yo().bro
True
>>> Yo().__honey
AttributeError: 'Yo' object has no attribute '__honey'
>>> Yo()._Yo__honey
True
```
2\.
```py
class Yo(object):
def __init__(self):
# Let's try something symmetrical this time
self.__honey__ = True
self.bro = True
```
**Output:**
```py
>>> Yo().bro
True
>>> Yo()._Yo__honey__
Traceback (most recent call last):
File "", line 1, in
AttributeError: 'Yo' object has no attribute '_Yo__honey__'
```
Why did `Yo()._Yo__honey` work?
2\.
```py
_A__variable = "Some value"
class A(object):
def some_func(self):
return __variable # not initiatlized anywhere yet
```
**Output:**
```py
Traceback (most recent call last):
File "", line 1, in
AttributeError: 'A' object has no attribute '__variable'
>>> >>> A().some_func()
'Some value'
```
3\.
```py
```
#### 💡 Explanation:
* [Name Mangling](https://en.wikipedia.org/wiki/Name_mangling) is used to avoid naming collisions between different namespaces.
* In Python, the interpreter modifies (mangles) the class member names starting with `__` (double underscore a.k.a "dunder") and not ending with more than one trailing underscore by adding `_NameOfTheClass` in front.
* So, to access `__honey` attribute in first snippet, we had to append `_Yo` to the front which would prevent conflicts with the same name attribute defined in any other class.
* But then why didn't it work in the second snippet? Because name mangling excludes the names ending with double underscores.
* The third snippet was also a consequence of name mangling. The name `__variable` in the statement `return __variable` was mangled to `_A__variable` which also happens to be the name of the variable we declared in outer scope.
---
---
## Section: Miscellaneous
### ▶ `+=` is faster
```py
# using "+", three strings:
>>> timeit.timeit("s1 = s1 + s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100)
0.25748300552368164
# using "+=", three strings:
>>> timeit.timeit("s1 += s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100)
0.012188911437988281
```
#### 💡 Explanation:
+ `+=` is faster than `+` for concatenating more than two strings because the first string (example, `s1` for `s1 += s2 + s3`) is not destroyed while calculating the complete string.
---
### ▶ Let's make a giant string!
```py
def add_string_with_plus(iters):
s = ""
for i in range(iters):
s += "xyz"
assert len(s) == 3*iters
def add_bytes_with_plus(iters):
s = b""
for i in range(iters):
s += b"xyz"
assert len(s) == 3*iters
def add_string_with_format(iters):
fs = "{}"*iters
s = fs.format(*(["xyz"]*iters))
assert len(s) == 3*iters
def add_string_with_join(iters):
l = []
for i in range(iters):
l.append("xyz")
s = "".join(l)
assert len(s) == 3*iters
def convert_list_to_string(l, iters):
s = "".join(l)
assert len(s) == 3*iters
```
**Output:**
```py
# Executed in ipython shell using %timeit for better readablity of results.
# You can also use the timeit module in normal python shell/scriptm=, example usage below
# timeit.timeit('add_string_with_plus(10000)', number=1000, globals=globals())
>>> NUM_ITERS = 1000
>>> %timeit -n1000 add_string_with_plus(NUM_ITERS)
124 µs ± 4.73 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
>>> %timeit -n1000 add_bytes_with_plus(NUM_ITERS)
211 µs ± 10.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit -n1000 add_string_with_format(NUM_ITERS)
61 µs ± 2.18 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit -n1000 add_string_with_join(NUM_ITERS)
117 µs ± 3.21 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> l = ["xyz"]*NUM_ITERS
>>> %timeit -n1000 convert_list_to_string(l, NUM_ITERS)
10.1 µs ± 1.06 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```
Let's increase the number of iterations by a factor of 10.
```py
>>> NUM_ITERS = 10000
>>> %timeit -n1000 add_string_with_plus(NUM_ITERS) # Linear increase in execution time
1.26 ms ± 76.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit -n1000 add_bytes_with_plus(NUM_ITERS) # Quadratic increase
6.82 ms ± 134 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit -n1000 add_string_with_format(NUM_ITERS) # Linear increase
645 µs ± 24.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit -n1000 add_string_with_join(NUM_ITERS) # Linear increase
1.17 ms ± 7.25 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> l = ["xyz"]*NUM_ITERS
>>> %timeit -n1000 convert_list_to_string(l, NUM_ITERS) # Linear increase
86.3 µs ± 2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```
#### 💡 Explanation
- You can read more about [timeit](https://docs.python.org/3/library/timeit.html) or [%timeit](https://ipython.org/ipython-doc/dev/interactive/magics.html#magic-timeit) on these links. They are used to measure the execution time of code pieces.
- Don't use `+` for generating long strings — In Python, `str` is immutable, so the left and right strings have to be copied into the new string for every pair of concatenations. If you concatenate four strings of length 10, you'll be copying (10+10) + ((10+10)+10) + (((10+10)+10)+10) = 90 characters instead of just 40 characters. Things get quadratically worse as the number and size of the string increases (justified with the execution times of `add_bytes_with_plus` function)
- Therefore, it's advised to use `.format.` or `%` syntax (however, they are slightly slower than `+` for very short strings).
- Or better, if already you've contents available in the form of an iterable object, then use `''.join(iterable_object)` which is much faster.
- `add_string_with_plus` didn't show a quadratic increase in execution time unlike `add_bytes_with_plus` because of the `+=` optimizations discussed in the previous example. Had the statement been `s = s + "x" + "y" + "z"` instead of `s += "xyz"`, the increase would have been quadratic.
```py
def add_string_with_plus(iters):
s = ""
for i in range(iters):
s = s + "x" + "y" + "z"
assert len(s) == 3*iters
>>> %timeit -n100 add_string_with_plus(1000)
388 µs ± 22.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit -n100 add_string_with_plus(10000) # Quadratic increase in execution time
9 ms ± 298 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
```
- So many ways to format and create a giant string are somewhat in contrast to the [Zen of Python](https://www.python.org/dev/peps/pep-0020/), according to which,
> There should be one-- and preferably only one --obvious way to do it.
---
### ▶ Explicit typecast of strings
```py
a = float('inf')
b = float('nan')
c = float('-iNf') #These strings are case-insensitive
d = float('nan')
```
**Output:**
```py
>>> a
inf
>>> b
nan
>>> c
-inf
>>> float('some_other_string')
ValueError: could not convert string to float: some_other_string
>>> a == -c #inf==inf
True
>>> None == None # None==None
True
>>> b == d #but nan!=nan
False
>>> 50/a
0.0
>>> a/a
nan
>>> 23 + b
nan
```
#### 💡 Explanation:
`'inf'` and `'nan'` are special strings (case-insensitive), which when explicitly typecasted to `float` type, are used to represent mathematical "infinity" and "not a number" respectively.
---
### ▶ Minor Ones
* `join()` is a string operation instead of list operation. (sort of counter-intuitive at first usage)
**💡 Explanation:**
If `join()` is a method on a string then it can operate on any iterable (list, tuple, iterators). If it were a method on a list, it'd have to be implemented separately by every type. Also, it doesn't make much sense to put a string-specific method on a generic `list` object API.
* Few weird looking but semantically correct statements:
+ `[] = ()` is a semantically correct statement (unpacking an empty `tuple` into an empty `list`)
+ `'a'[0][0][0][0][0]` is also a semantically correct statement as strings are [sequences](https://docs.python.org/3/glossary.html#term-sequence)(iterables supporting element access using integer indices) in Python.
+ `3 --0-- 5 == 8` and `--5 == 5` are both semantically correct statements and evaluate to `True`.
* Given that `a` is a number, `++a` and `--a` are both valid Python statements but don't behave the same way as compared with similar statements in languages like C, C++ or Java.
```py
>>> a = 5
>>> a
5
>>> ++a
5
>>> --a
5
```
**💡 Explanation:**
+ There is no `++` operator in Python grammar. It is actually two `+` operators.
+ `++a` parses as `+(+a)` which translates to `a`. Similarly, the output of the statement `--a` can be justified.
+ This StackOverflow [thread](https://stackoverflow.com/questions/3654830/why-are-there-no-and-operators-in-python) discusses the rationale behind the absence of increment and decrement operators in Python.
* Python uses 2 bytes for local variable storage in functions. In theory, this means that only 65536 variables can be defined in a function. However, python has a handy solution built in that can be used to store more than 2^16 variable names. The following code demonstrates what happens in the stack when more than 65536 local variables are defined (Warning: This code prints around 2^18 lines of text, so be prepared!):
```py
import dis
exec("""
def f():
""" + """
""".join(["X"+str(x)+"=" + str(x) for x in range(65539)]))
f()
print(dis.dis(f))
```
* Multiple Python threads won't run your *Python code* concurrently (yes you heard it right!). It may seem intuitive to spawn several threads and let them execute your Python code concurrently, but, because of the [Global Interpreter Lock](https://wiki.python.org/moin/GlobalInterpreterLock) in Python, all you're doing is making your threads execute on the same core turn by turn. Python threads are good for IO-bound tasks, but to achieve actual parallelization in Python for CPU-bound tasks, you might want to use the Python [multiprocessing](https://docs.python.org/2/library/multiprocessing.html) module.
* List slicing with out of the bounds indices throws no errors
```py
>>> some_list = [1, 2, 3, 4, 5]
>>> some_list[111:]
[]
```
* `int('١٢٣٤٥٦٧٨٩')` returns `123456789` in Python 3. In Python, Decimal characters include digit characters, and all characters that can be used to form decimal-radix numbers, e.g. U+0660, ARABIC-INDIC DIGIT ZERO. Here's an [interesting story](http://chris.improbable.org/2014/8/25/adventures-in-unicode-digits/) related to this behavior of Python.
* `'abc'.count('') == 4`. Here's an approximate implementation of `count` method, which would make the things more clear
```py
def count(s, sub):
result = 0
for i in range(len(s) + 1 - len(sub)):
result += (s[i:i + len(sub)] == sub)
return result
```
The behavior is due to the matching of empty substring(`''`) with slices of length 0 in the original string.
---
# Contributing
All patches are Welcome! Please see [CONTRIBUTING.md](/CONTRIBUTING.md) for further details.
For discussions, you can either create a new [issue](https://github.com/satwikkansal/wtfpython/issues/new) or ping on the Gitter [channel](https://gitter.im/wtfpython/Lobby)
# Acknowledgements
The idea and design for this collection were initially inspired by Denys Dovhan's awesome project [wtfjs](https://github.com/denysdovhan/wtfjs). The overwhelming support by the community gave it the shape it is in right now.
#### Some nice Links!
* https://www.youtube.com/watch?v=sH4XF6pKKmk
* https://www.reddit.com/r/Python/comments/3cu6ej/what_are_some_wtf_things_about_python
* https://sopython.com/wiki/Common_Gotchas_In_Python
* https://stackoverflow.com/questions/530530/python-2-x-gotchas-and-landmines
* https://stackoverflow.com/questions/1011431/common-pitfalls-in-python
* https://www.python.org/doc/humor/
* https://www.codementor.io/satwikkansal/python-practices-for-efficient-code-performance-memory-and-usability-aze6oiq65
# 🎓 License
[![CC 4.0][license-image]][license-url]
© [Satwik Kansal](https://satwikkansal.xyz)
[license-url]: http://www.wtfpl.net
[license-image]: https://img.shields.io/badge/License-WTFPL%202.0-lightgrey.svg?style=flat-square
## Help
If you have any wtfs, ideas or suggestions, please share.
## Surprise your geeky pythonist friends?
You can use these quick links to recommend wtfpython to your friends,
[Twitter](https://twitter.com/intent/tweet?url=https://github.com/satwikkansal/wtfpython&hastags=python,wtfpython)
| [Linkedin](https://www.linkedin.com/shareArticle?url=https://github.com/satwikkansal&title=What%20the%20f*ck%20Python!&summary=An%20interesting%20collection%20of%20subtle%20and%20tricky%20Python%20snippets.)
## Need a pdf version?
I've received a few requests for the pdf version of wtfpython. You can add your details [here](https://satwikkansal.xyz/wtfpython-pdf/) to get the pdf as soon as it is finished.