{"id":18627504,"url":"https://github.com/yugr/python-hate","last_synced_at":"2026-03-11T01:31:47.738Z","repository":{"id":60186807,"uuid":"138522212","full_name":"yugr/python-hate","owner":"yugr","description":"A growing list of things I dislike about Python","archived":false,"fork":false,"pushed_at":"2025-03-03T21:10:31.000Z","size":44,"stargazers_count":55,"open_issues_count":1,"forks_count":3,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-11T14:05:04.947Z","etag":null,"topics":["hate","python"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yugr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2018-06-24T23:19:07.000Z","updated_at":"2025-04-07T09:27:21.000Z","dependencies_parsed_at":"2025-01-02T07:30:30.359Z","dependency_job_id":"5b16d05c-52da-491d-83e7-8cccec6bd36b","html_url":"https://github.com/yugr/python-hate","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/yugr/python-hate","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yugr%2Fpython-hate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yugr%2Fpython-hate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yugr%2Fpython-hate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yugr%2Fpython-hate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yugr","download_url":"https://codeload.github.com/yugr/python-hate/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yugr%2Fpython-hate/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30366051,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-10T21:41:54.280Z","status":"ssl_error","status_checked_at":"2026-03-10T21:40:59.357Z","response_time":106,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hate","python"],"created_at":"2024-11-07T04:42:36.916Z","updated_at":"2026-03-11T01:31:47.694Z","avatar_url":"https://github.com/yugr.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"A growing list of things I dislike about Python.\n\nThere are workarounds for some of them (often half-broken and usually unintuitive)\nand others may even be considered to be virtues by some people.\n\n# Generic\n\n## Zero static checking\n\nThe most critical problem of Python is complete lack of static checking\n(it does not even detect missing variable definitions)\nwhich increases debugging time and makes refactoring more time-consuming than needed.\nThis becomes particularly obvious when you run your app on a huge amount of data overnight,\njust to detect missing initialization in some rarely called function in the morning.\n\nThere is [Pylint](https://www.pylint.org) but it is a linter (i.e. style checker)\nrather than a real static analyzer so it is unable, by design, to detect\nmany serious errors which require dataflow analysis.\nFor example if fails on basic stuff like\n* invalid string formatting (fixed [here](https://github.com/PyCQA/pylint/pull/2465))\n* iterating over unsorted dicts (reported [here](https://github.com/PyCQA/pylint/issues/2467) with draft patch, rejected because maintainers consider it unimportant (no particular reasons provided))\n* dead list computations (e.g. using `sorted(lst)` instead of `lst.sort()`)\n* modifying list while iterating over it (reported [here](https://github.com/PyCQA/pylint/issues/2471) with draft patch, rejected because maintainers consider it unimportant (no particular reasons provided))\n* etc.\n\n## Global interpreter lock\n\n[GIL](https://wiki.python.org/moin/GlobalInterpreterLock) precludes high-performant multithreading\nwhich is suprising at the age of multicores.\n\n## No type annotations\n\n_Type annotations have finally been introduced in Python 3.5. Google has even developed a [pytype](https://github.com/google/pytype) type inferencer/checker but it seems to have [serious](https://github.com/google/pytype/issues/581) [limitations](https://github.com/google/pytype/issues/580) so it's unclear whether it's production-ready._\n\nLack of type annotations forces people to use hungarian notation\nin complex programs (hello 90-s!).\n\n# Language\n\n## Negation operator syntax issue\n\nThis is not a valid syntax:\n```\nx == not y\n```\n\n## Unable to overload logic operators\n\nIt's not possible to overload `and`, `or` or `not` (which might have been handy to represent e.g. operations on set-like or geometric objects).\nThere's even a [PEP](https://www.python.org/dev/peps/pep-0335/) which was rejected\nbecause Guido disliked particular implementation.\n\n## Hiding type errors via (un)helpful conversions\n\nIt's very easy to make a mistake of writing `len(lst1) == lst2`\ninstead of intended `len(lst1) == len(lst2)`.\nPython will (un)helpfully make it harder to find this error\nby silently converting first variant to `[len(lst1)] * len(lst2) == lst2`\n(instead of aborting with a type fail).\n\n## Limited lambdas\n\nFor unclear reason lambda functions only support expressions\nso anything that has control flow requires a local named function.\n\n[PEP 3113](https://www.python.org/dev/peps/pep-3113/)\ntries to persuade you that this was a good design decision:\n```\nWhile an informal poll of the handful of Python programmers I know personally ...\nindicates a huge majority of people do not know of this feature ...\n```\n\n## Problematic operator precedence\n\nThe `is` and `is not` operators have the same precedence as comparisons\nso this code\n```\nop.post_modification is None != full_op.post_modification is None\n```\nwould rather unexpectedly evalute as\n```\n((op.post_modification is None) != full_op.post_modification) is None\n```\n\n## The useless self\n\nExplicitly writing out `self` in all method declarations and calls\nshould not be needed. Apart from being an unnecessary boilerplate,\nthis enables another class of bugs:\n```\nclass A:\n  def first(x, *y):\n    return x\n\na = A\nprint(a.first(1,2,3))  # Will print a, not 1\n```\n\n## Optional parent constructors\n\nPython does not require a call to parent class constructor:\n```\nclass A(B):\n  def __init__(self, x):\n    super().__init__()\n    self.x = x\n```\nso when it's missing you'll have hard time understanding\nwhether it's been omitted deliberately or accidentally.\n\n## Inconsistent syntax for tuples\n\nPython allows omission of parenthesis around tuples in most cases:\n```\nfor i, x in enumerate(xs):\n  pass\n\nx, y = y, x\n\nreturn x, y\n```\nbut not all cases:\n```\nfoo = [x, y for x in range(5) for y in range(5)]\nSyntaxError: invalid syntax\n```\n\n## No tuple unpacking in lambdas\n\nIt's not possible to do tuple unpacking in lambdas so instead\nof concise and readable\n```\nlst = [(1, 'Helen', None), (3, 'John', '121')]\nlst.sort(key=lambda n, name, phone: (name, phone))  # TypeError: \u003clambda\u003e() missing 2 required positional arguments\n```\nyou should use\n```\nlst.sort(key=lambda n_name_phone: (n_name_phone[1], n_name_phone[2]))\n```\n\nThis seems to be intentional decision as tuple unpacking does work in Python 2.\n\n## Inconsistent syntax of set literals\n\nSets can be initialized via syntactic sugar:\n```\n\u003e\u003e\u003e x = {1, 2, 3}\n\u003e\u003e\u003e type(x)\n\u003cclass 'set'\u003e\n```\nbut it breaks for empty sets:\n```\n\u003e\u003e\u003e x = {}\n\u003e\u003e\u003e type(x)\n\u003cclass 'dict'\u003e\n```\n\n## Inadvertent sharing\n\nIt's too easy to inadvertently share references:\n```\na = b = []\n```\nor\n```\ndef foo(x=[]):  # foo() will return [1], [1, 1], [1, 1, 1], etc.\n  x.append(1)\n  return x\n```\nor even\n```\ndef foo(obj, lst=[]):\n  obj.lst = lst\nfoo(obj)\nobj.lst.append(1)  # Hoorah, this modifies default value of foo\n```\n\n## Functions always return\n\nDefault return value from function (when `return` is omitted) is `None`.\nThis makes it impossible to declare subroutines which are not supposed\nto return anything (and verify this at runtime).\n\n## Automatic field insertion\n\nAssigning a non-existent object field adds it instead of throwing an exception:\n```\nclass A:\n  def __init__(self):\n    self.x = 0\n...\na = A()\na.y = 1  # OK\n```\n\nThis complicates refactoring because forgetting to update an outdated field name\ndeep inside your (or your colleague's) program will silently work,\nbreaking your program much later.\n\nThis can be overcome with `__slots__` but when have you seen them\nused last time?\n\n## Hiding class variables in instances\n\nWhen accessing object attribute via `obj.attr` syntax Python will first search\nfor `attr` in `obj`'s instance variables. If it's not present,\nit will search `attr` in class variables of `obj`'s class.\n\nThis behavior is reasonable and matches other languages.\n\nProblem is that status of `attr` will change if we write it:\n```\nclass A:\n  x = 1\n\na = A()\n\n# Here self.v means A.v ...\nprint(A.x) # 1\nprint(a.x) # 1\n\n# ... and here it does not\na.x = 2\nprint(A.x) # 1\nprint(a.x) # 2\n```\n\nThis leads to this particularly strange and unexpected semantics:\n```\nclass A:\n  x = 1\n\n  def __init__(self):\n    self.x += 1\n\nprint(A.x)  # 1\n\na = A()\nprint(A.x)  # 1\nprint(a.x)  # 2\n```\nTo understand what's going on, note that `__init__` is interpreted as\n```\ndef __init__(self):\n  self.x = A.x + 1\n```\n\n## \"Is\" operator does not work for primitive types\n\nNo comments:\n```\n\u003e 2+2 is 4\nTrue\n\u003e 999+1 is 1000\nFalse\n```\n\nThis happens because [only sufficiently small integer objects are reused](https://stackoverflow.com/a/306353/2170527):\n```\n# Two different instances of number \"1000\"\n\u003e\u003e\u003e id(999+1)\n140068481622512\n\u003e\u003e\u003e id(1000)\n140068481624112\n\n# Single instance of number \"4\"\n\u003e\u003e\u003e id(2+2)\n10968896\n\u003e\u003e\u003e id(4)\n10968896\n```\n\n## Inconsistent index checks\n\nInvalid indexing throws an exception\nbut invalid slicing does not:\n```\n\u003e\u003e\u003e a=list(range(4))\n\u003e\u003e\u003e a[4]\nTraceback (most recent call last):\n  File \"\u003cstdin\u003e\", line 1, in \u003cmodule\u003e\nIndexError: list index out of range\n\u003e\u003e\u003e a[4:5]\n[]\n```\n\n## Spurious `:`s\n\nPython got rid of spurious bracing but introduced a spurious `:` lexeme instead.\nThe lexeme is not needed for parsing and its only purpose\n[was](http://effbot.org/pyfaq/why-are-colons-required-for-the-if-while-def-class-statements.htm)\nto somehow \"enhance readability\".\n\n## Unnatural operator priority\n\nNormally all unary operators have higher priority than binary ones but of course not in Python:\n```\n\u003e\u003e\u003e not 'x' in ['x', False]\nFalse\n\u003e\u003e\u003e (not 'x') in ['x', False]\nTrue\n\u003e\u003e\u003e not ('x' in ['x', False])\nFalse\n```\n\nA funny consequence of this is that `x not in lst` and `not x in lst` notations are equivalent.\n\n## Weird semantics of `super()`\n\nWhen you call `super().__init__` in your class constructor:\n```\nclass B(A):\n  def __init__(self):\n    super(B, self).__init__()\n```\nit will NOT necessarily call constructor of superclass (`A` in this case).\n\nInstead it will call a constructor of _some other_ class from class hierarchy\nof `self`s class (if this sounds a bit complicated that's because it actually is).\n\nLet's look at a simple example:\n```\n#   object\n#   /    \\\n#  A      B\n#  |      |\n#  C      D\n#   \\    /\n#      E\n\nclass A(object):\n  def __init__(self):\n    print(\"A\")\n    super().__init__()\n\nclass B(object):\n  def __init__(self):\n    print(\"B\")\n    super().__init__()\n\nclass C(A):\n  def __init__(self, arg):\n    print(f\"C {arg}\")\n    super().__init__()\n\nclass D(B):\n  def __init__(self, arg):\n    print(f\"D {arg}\")\n    super().__init__()\n\nclass E(C, D):\n  def __init__(self, arg):\n    print(f\"E {arg}\")\n    super().__init__(arg)\n```\nIf we try to construct an instance of `E` we'll get a puzzling error:\n```\n\u003e\u003e\u003e E(10)\nE 1\nC 1\nA\nTraceback (most recent call last):\n  File \"\u003cstdin\u003e\", line 1, in \u003cmodule\u003e\n  File \"\u003cstdin\u003e\", line 4, in __init__\n  File \"\u003cstdin\u003e\", line 4, in __init__\n  File \"\u003cstdin\u003e\", line 4, in __init__\nTypeError: __init__() missing 1 required positional argument: 'arg'\n```\nWhat happens here is that for diamond class hierarchies Python will\nexecute constructors in strange unintuitive order\n(called MRO, explained in details [here](https://docs.python.org/3/howto/mro.html)).\nIn our case the order happens to be E, C, A, D, B.\n\nAs you can see poor `A` suddenly has to call `D` instead of expected `object`.\n`D` requires `arg` of which `A` is of course not aware of, hence the crash.\nSo when you call `super()` you have no idea which class you are calling into,\nnor what the expected `__init__` signature is.\n\nWhen using `super()` ALL your `__init__` methods have to use keyword arguments only\nand pass all of them to the caller:\n```\nclass A(object):\n  def __init__(self, **kwargs):\n    print(\"A\")\n    if type(self).__mro__[-2] is A:\n      # Avoid \"TypeError: object.__init__() takes no parameters\" error\n      super().__init__()\n      return\n    super().__init__(**kwargs)\n\nclass B(object):\n  def __init__(self, **kwargs):\n    print(\"B\")\n    if type(self).__mro__[-2] is B:\n      # Avoid \"TypeError: object.__init__() takes no parameters\" error\n      super().__init__()\n      return\n    super().__init__(**kwargs)\n\nclass C(A):\n  def __init__(self, **kwargs):\n    arg = kwargs[\"arg\"]\n    print(f\"C {arg}\")\n    super().__init__(**kwargs)\n\nclass D(B):\n  def __init__(self, **kwargs):\n    arg = kwargs[\"arg\"]\n    print(f\"D {arg}\")\n    super().__init__(**kwargs)\n\nclass E(C, D):\n  def __init__(self, **kwargs):\n    arg = kwargs[\"arg\"]\n    print(f\"E {arg}\")\n    super().__init__(**kwargs)\n\nE(arg=1)\n```\nNotice the especially beautiful `__mro__` checks which I needed to avoid error in `object.__init_`\nwhich just so happens to NOT support the kwargs convention\n(see [super() and changing the signature of cooperative methods](https://stackoverflow.com/questions/56714419/super-and-changing-the-signature-of-cooperative-methods)\nfor details).\n\nI'll let the reader decide how more readable and efficient this makes your code.\n\nSee [Python's Super is nifty, but you can't use it](https://fuhm.net/super-harmful/) for an in-depth discussion.\n\n# Standard Libraries\n\n## List generators fail check for emptiness\n\nThanks to active use of generators in Python 3 it became easier\nto misuse standard APIs:\n```\nif filter(lambda x: x == 0, [1,2]):\n  print(\"Yes\")  # Prints \"Yes\"!\n```\nSurpisingly enough this does not apply to `range` (i.e. `bool(range(0))` returns `False` as expected).\n\n## Argparse does not support negated flags\n\n`argparse` does not provide automatic support for `--no-XXX` flags.\n\n## Argparse has useless default formatter\n\nBy default formatter used by argparse\n* won't display default option values\n* will make code examples provided via `epilog` unreadable\n  by stripping leading whitespaces\n\nEnabling both features requires defining a custom formatter:\n```\nclass Formatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter):\n  pass\n```\n\n## Getopt does not allow parameters after positional arguments\n\n```\n\u003e\u003e\u003e opts, args = getopt.getopt(['A', '-o', 'B'], 'o:')\n\u003e\u003e\u003e opts\n[]\n\u003e\u003e\u003e args\n['A', '-o', 'B']\n\u003e\u003e\u003e opts, args = getopt.getopt(['-o', 'B', 'A'], 'o:')\n\u003e\u003e\u003e opts\n[('-o', 'B')]\n\u003e\u003e\u003e args\n['A']\n```\n\n## Split and join disagree on argument order\n\n`split` and `join` accept list and separator in different order:\n```\nsep.join(lst)\nlst.split(sep)\n```\n\n## Builtins do not behave as normal functions\n\nBuiltin functions [do not support named arguments](https://stackoverflow.com/a/24463222/2170527)\ne.g.\n```\n\u003e\u003e\u003e x = {1: 2}\n\u003e\u003e\u003e x.get(2, 0)\n0\n\u003e\u003e\u003e x.get(2, default=0)\nTraceback (most recent call last):\n  File \"\u003cstdin\u003e\", line 1, in \u003cmodule\u003e\nTypeError: get() takes no keyword arguments\n```\n\n## Inconsistent naming\n\n`string.strip()` and `list.sort()`, although named similarly (a verb in imperative mood),\nhave very different behavior: string's method returns a stripped copy whereas\nlist's one sorts object inplace (and returns `None`).\n\n## Path concatenation may silently ignore inputs\n\n`Os.path.join` will [silently drop preceding inputs on argument with leading slash](https://stackoverflow.com/questions/1945920/why-doesnt-os-path-join-work-in-this-case):\n```\n\u003e\u003e\u003e print(os.path.join('/home/yugr', '/../libexec'))\n/../libexec\n```\n\nPython docs mentions this behavior:\n\u003e If a component is an absolute path, all previous components are thrown away and joining continues from the absolute path component.\n\nbut unfortunately do not provide any reasons for such irrational choice. Throwing exception would be much less error-prone.\n\nGoogle search for [why os.path.join throws away](https://www.google.com/search?q=why+os.path.join+throws+away+site%3Astackoverflow.com) returns 22K results at the time of this writing...\n\n## Different rounding algorithm\n\nPython uses a non-standard banker's rounding algorithm:\n```\n# Python3 (bankers rounding)\n\u003e\u003e\u003e round(0.5)\n0\n\u003e\u003e\u003e round(1.5)\n2\n\u003e\u003e\u003e round(2.5)\n2\n```\nApart from being counterintuitive, this is also different from most other languages (C, Java, Ruby, etc.).\n\n# Name Resolution\n\n## No way to localize a name\n\nPython lacks lexical scoping i.e. there is no way to localize variable\nin a scope smaller than a function.\nThis often hurts when renaming variables during code refactoring.\nForgetting to rename variable name in a single place, causes interpreter\nto pick up an unrelated name from unrelated block 50 lines above or\nfrom previous loop iteration.\n\nThis is especially inconvenient for one-off variables (e.g. loop counters):\n```\nfor i in range(100):\n  ...\n\n# 100 lines later\n\nfor j in range(200):\n  a[i] = 0  # Yikes, forgot to rename!\n```\n\nOne could remove a name from scope via `del i` but this is considered too\nverbose so noone uses it.\n\n## Everything is exported\n\nSimilarly to above, there is no control over visibility (can't hide class methods,\ncan't hide module functions). You are left with a _convention_ to precede\nprivate functions with `_` and hope for the best.\n\n## Multiple syntaxes for aliasing imported module\n\nPython allows different syntaxes for the aliasing functionality:\n```\nfrom mod import submod as X\nimport mod.submod as X\n```\n\n## Assignment makes variable local\n\nPython scoping rules require that assigning a variable automatically declares it local.\nThis causes inconsistencies and weird limitations in practice. E.g. variable would\nbe considered local even if assignment follows first use:\n```\nglobal xxx\nxxx = 0\n\ndef foo():\n  a = xxx  # Throws UnboundLocalError\n  xxx = 2\n```\nand even if assignment is never ever executed:\n```\ndef foo():\n  a = xxx  # Still aborts...\n  if False:\n    xxx = 2\n```\nThis is particularly puzzling in long functions when someone accidentally\nadds a local variable which matches name of a global variable used in other part\nof the same function:\n```\ndef value():\n  ...\n\ndef foo():\n  ...  # A lot of code\n  value(1)  # Surprise! UnboundLocalError\n  ...  # Yet more code\n  for value in data:\n    ...\n```\n\nOnce you've lost some time debugging the issue, you can be overcome\nfor global variables by declaring their names as `global`\nbefore first use:\n```\ndef foo():\n  global xxx\n  a = xxx\n  xxx = 2\n```\nBut there are no magic keywords forvariables from non-global outer scopes so they\nare essentially unwritable from nested scopes i.e. closures:\n```\ndef foo():\n  xxx = 1\n  def bar():\n    xxx = 2  # No way to modify xxx...\n```\n\nThe only available \"solution\" is to wrap the variable into a fake 1-element array\n(whaat?!):\n```\ndef foo():\n  xxx = [1]\n  def bar():\n    xxx[0] = 2\n```\n\n## Unreachable code that gets executed\n\nNormally statements that belongs to false branches are not executed.\nE.g. this code works:\n```\nif True:\n  import re\nre.search(r'1', '1')\n```\nand this one raises `NameError`:\n```\nif False:\n  import re\nre.search(r'1', '1')\n```\nThis does not apply to `global` declarations though:\n```\nxxx = 1\ndef foo():\n  if False:\n    global xxx\n  xxx = 42\nfoo()\nprint(xxx)  # Prints 42\n```\n\n## Relative imports are unusable\n\nRelative imports (`from .xxx.yyy import mymod`) have many weird limitations\ne.g. they will not allow you to import module from parent folder and\nthey will seize work in main script\n```\nModuleNotFoundError: No module named '__main__.xxx'; '__main__' is not a package\n```\n\nA workaround is to use extremely ugly `sys.path` hackery:\n```\nimport sys\nimport os.path\nsys.path.append(os.path.join(os.path.dirname(__file__), 'xxx', 'yyy'))\n```\n\nSearch for \"python relative imports\" on stackoverflow to see some really clumsy Python code\n(e.g. [here](https://stackoverflow.com/questions/279237/import-a-module-from-a-relative-path)\nor [here](https://stackoverflow.com/questions/1918539/can-anyone-explain-pythons-relative-imports)).\nAlso see [When are circular imports fatal?](https://datagrok.org/python/circularimports/)\nfor more weird limitations of relative imports with respect to circular dependencies.\n\n# Performance\n\n## Automatic optimization is hard\n\nIt's very hard to automatically optimize Python code\nbecause there are far too many ways\nin which program may change execution environment e.g.\n```\n  for re in regexes:\n    ...\n```\n(see e.g. [this quote](https://youtu.be/2wDvzy6Hgxg?t=690) from Guido).\nExisting optimizers (e.g. pypy) have to rely on idioms and heuristics.\n\n# Infrastructure\n\n## Syntax checking\n\nSyntax error reporting in Python is extremely primitive.\nIn most cases you simply get `SyntaxError: invalid syntax`.\n\n## Different conventions on OSes\n\nWindows and Linux use different naming convention for Python executables\n(`python` on Windows, `python2`/`python3` on Linux).\n\n## Debugger is slow\n\nPython debugging is super-slow (few orders of magnitude slower than\ninterpretation).\n\n## No static analyzer\n\nAlready mentioned in [Zero static checking](#zero-static-checking).\n\n## Unable to break on pass statement\n\nPython debugger will [ignore breakpoints set on pass statements](https://stackoverflow.com/a/47626134/2170527).\nThus poor-man's conditional breakpoints like\n```\nif x \u003e 0:\n  pass\n```\nwill silently fail to work, leaving a false impression that condition is always false.\n\n## Semantic changes in Python 3\n\nMost people blame Python 3 for syntax changes which break existing code\n(e.g. making `print` a regular function) but the real problem is\n_semantic_ changes as they are much harder to detect and debug.\nSome examples\n* integer division:\n```\nprint(1/2)  # Prints \"0\" in 2, \"0.5\" in 3\n```\n* checking `filter`ed list for emptiness:\n```\nif filter(lambda x: x, [0]):\n  print(\"X\")  # Executes in 3 but not in 2\n```\n* order of keys in dictionary is random until Python 3.6\n* different rounding algorithm:\n```\n# Python2.7\n\u003e\u003e\u003e round(0.5)\n1.0\n\u003e\u003e\u003e round(1.5)\n2.0\n\u003e\u003e\u003e round(2.5)\n3.0\n\n# Python3 (bankers rounding)\n\u003e\u003e\u003e round(0.5)\n0\n\u003e\u003e\u003e round(1.5)\n2\n\u003e\u003e\u003e round(2.5)\n2\n```\n\n## Dependency hell\n\nPython community does not seem to have a strong culture of preserving API backwards compatibility\nor following [SemVer convention](https://semver.org/)\n(which is hinted by the fact that there are no widespread tools for checking Python package API\ncompatibility). This is not surprising given that even minor versions of Python 3 itself\nbreak old and popular APIs (e.g. [time.clock](https://bugs.python.org/issue31803)).\nAnother likely reason is lack of good mechanisms to control what's exported from module\n(prefixing methods and objects with underscore is _not_ a good mechanism).\n\nIn practice this means that it's too risky to allow differences in minor (and even patch) versions\nof dependencies.\nInstead the most robust (and thus most common) solution is to fix _all_ app dependencies\n(including the transitive ones) down to patch versions (via blind `pip freeze \u003e requirements.txt`)\nand run each app in a dedicated virtualenv or Docker container.\n\nApart from complicating deployment, fixing versions also\ncomplicates importing module in other applications\n(due to increased chance of conflicing dependencies)\nand upgrading dependencies later on to get bugfixes and security patches.\n\nFor more details see the excellent [\"Dependency hell: a library author's guide\" talk](https://www.youtube.com/watch?v=OaBhcueqNqw)\nand an alternative view in [Should You Use Upper Bound Version Constraints?](https://iscinumpy.dev/post/bound-version-constraints/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyugr%2Fpython-hate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyugr%2Fpython-hate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyugr%2Fpython-hate/lists"}