Web Architecture, Java Ecosystem, Software Craftsmanship

I like Python! Confessions of a Java Developer

Posted on Oct 2, 2016

Developing with Python was a refreshing and pleasant experience. After working with Java for a while, you may forget how verbose and clumsy this language is sometimes. But Python shows how simple and powerful a programming language can be. Let me show you some examples.

Python Logo

TL;DR

What makes Python so cool?

  • Concise and powerful syntax. It’s almost like pseudo-code.
  • Powerful collection support.
  • Awesome list comprehension. That’s my personal highlight.
  • Named and default parameter.
  • Rich ecosystem and an active community.
  • Incredible speed when it comes to IO or database operations.
  • Functions are first-class citizens.
  • Exceptions are always unchecked! (Well, that’s normal for a dynamically typed language, but I just want to express my enthusiasm.)

What makes a Java developer feel home?

  • Pip for dependency management, PyPI as the central artifact repository and PyBuilder for project setup, test execution and build management.
  • Type Hinting improves IDE support (up to a certain extent)
  • Python code is also compiled. But it usually happens on-the-fly so you won’t notice.

But Python is still a dynamically typed language. That means:

  • Errors only show up at runtime. So it’s more likely to ship defect software.
  • You spend much time for goggling API.
  • IDE support simply can’t be that good. But IntelliJ does its best.
  • Refactorings can be dangerous and error-prone.
  • You’ll need more tests because errors will only come up at runtime.
  • No encapsulation and information hiding because everything is public.

Python is awesome for advanced scripting and small applications. But I won’t use it for applications that are performance-critical, contain complex business logic or require long-term maintainability and sustainability.

Basics

# You don’t need {} for method bodies (use indent instead), no semicolon, no () in if conditions.
def divide(a, b):
    if b == 0:
        raise ValueError("Dude, you can't divide {} by {}".format(a, b))
    return a / b

# more intuitive than Java's ternary operator
parameter = "Peter"
name = "Unknown" if parameter is None else parameter

# multi-line strings. praise the lord!
description = """This is my
multi-line string. """

# chained comparison
if 200 <= status_code < 300:
    print("success")

Collections

Python is just awesome when it comes to collections.

# nice literals for list, tuple, sets, dicts
my_list = [1, 2, 3]
my_tuple = (1, 2)
my_set = {1, 2, 2}
my_dict = {"a": 1, "b": 2}
# this collection literals makes dealing with JSON a pleasure.
# this is also the reason why the Python driver for MongoDB is definitely more fun.

# contains
contains_element = 1 in my_list  # True
contains_key = "a" not in my_dict  # False

# access
value = my_list[0]
value2 = my_dict["a"]

# iteration
for element in my_list:
    print(element)

for index, element in enumerate(my_list):  # foreach with index!
    print(index, element)

for key, value in my_dict.items():  # how cool is that?!
    print(key, value)

# list can be used as stacks...
print(my_list.pop())  # 3 (remove and return element)
# ...and as queues
print(my_list.pop(0))  # 1 (remove and return first element)

# == List Comprehension ==
# That's my personal kick-ass feature of Python
# It's like map() and filter() of the Java 8 Stream API, but much better.
# syntax: [<map_function> for <element> in <collection> if <condition>]
# example:
names = ["peter", "paul", "florian", "albert"]
# filter for names starting with "p" (filter()) and upper case them (map())
result = [name.upper() for name in names if name.startswith("p")]
print(result)  # ["PETER", "PAUL"]
# and it returns... a new list! no annoying collect(Collectors.toList()) boilerplate.
# I really love this powerful and concise syntax.

# slice syntax for extracting parts of a collection
# syntax: collection[startIndex:stopIndex(:step)]
# startIndex defaults to 0; stopIndex defaults to len(col); step defaults to 1, negative step reverses direction of iteration
print(names[1:2])  # paul
print(names[1:])  # all except the first: paul, florian, albert
print(names[:1])  # get first element: peter
print(names[:])  # copy whole collection
print(names[::-1])  # reverse list: ['albert', 'florian', 'paul', 'peter']
# works also for strings
print("hello nice world"[6:10])  # nice

Functions

def copy(source_file, target_file, override):
    pass  # imagine some code here...

# unclear what the parameters mean:
copy("~/file.txt", "~/file2.txt", True)
# named parameters make method calls readable!
copy(source_file="~/file.txt", target_file="~/file2.txt", override=True)
# it's also more secure, because an error is thrown, if the argument name doesn't exist

# default parameters! no silly constructor chaining.
def copy2(source_file, target_file, override = True):
    pass  # imagine some code here...
copy2(source_file="~/file.txt", target_file="~/file2.txt")

# functions are first class citizens. You can assign them to variables, return and pass them around
my_copy = copy2

# lambdas
get_year = lambda date: date.year
import datetime
today = datetime.date.today()
some_day = datetime.date(2010, 9, 15)
for date in [today, some_day]:
    print(get_year(date))  # 2016, 2010

# multiple return values via automatic tuple packing and unpacking
def multiple_return():
    return 1, 2, True  # will be packed to a tuple
print(multiple_return())  # (1, 2, True)

first, second, third = multiple_return()  # unpacking
print(first, second, third)  # 1 2 True

Classes

# let's define a User class with the properties name and age
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def introduce_yourself(self):
        print("Hi, I'm {}, {} years old".format(self.name, self.age))

# create a User
user = User("Hauke", 28)
print(user.name)  # Hauke
user.introduce_yourself()  # Hi, I'm Hauke, 28 years old

# by the way there is also Inheritance and Polymorphism

Operator Overloading

import datetime
today = datetime.date.today()
some_day = datetime.date(2016, 9, 15)
print(today - some_day)  # "17 days, 0:00:00". difference between dates.

set1 = {1, 2, 3}
set2 = {3, 4, 5}
print(set2 - set1)  # {4, 5}. difference between sets

# Let's define our own overloading
# Therefore, we use Protocols. Protocols define method signatures. If you implement these methods, you can use certain operators.
# e.g. for "==" we need to implement "__eq__"
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __eq__(self, other):
        return self.name is other.name and self.age is other.age

print(User("Hauke", 28) == User("Peter", 30))  # False

# e.g. for "list[element]" we need to implement "__getitem__"
class UserAgeFinder:
    def __init__(self):
        self.user_list = [User("Hauke", 28), User("Peter", 30), User("Hugo", 55)]

    def __getitem__(self, name):
        return [user.age for user in self.user_list if user.name is name]

finder = UserAgeFinder()
print(finder["Hauke"])  # [28]

Type Hinting + IntelliJ PyCharm

Python 3.5 introduced type hints. It’s important to note that they are just hints. The Python interpreter won’t complain about invalid methods and arguments. It just for tooling and documentation.

IntelliJ PyCharm provides nice auto completion based on type hints. In the following example PyCharm shows us the methods of a string (str).

Python Type Hints enable IntelliJ PyCharm to provide nice autocompletion

Python Type Hints enable IntelliJ PyCharm to provide nice autocompletion

The auto completion works also for our custom classes.

Moreover, PyCharm tells us when we invoke an invalid method or property.

Python Type Hints enable IntelliJ PyCharm to show errors in case of invalid method invocations

Python Type Hints enable IntelliJ PyCharm to show errors in case of invalid method invocations

Development Tools

  • REPL: The interactive mode of the Python interpreter is useful for ad-hoc testing of code snippets.
  • pip: Dependency management.
  • PyPI: Python’s artifact repository. It’s Python’s Maven Central.
  • PyBuilder: Build management. Every Maven user will feel home immediately. PyBuilder sets up a standard project structure (src/main/python ;-)), executes tests and finally builds  an artifact that can be installed via pip. Nice.
  • venv: Install your project dependencies into a virtual environment to avoid pollution of your system. Useful if you have multiple projects with different dependencies.
  • IntelliJ PyCharm: Once again IntelliJ provides an awesome IDE. It supports type hints, venv, requirements.txt and PEP (= Python’s strict coding style guide). Moreover, it points to mistakes and best practices during development (as good as it can be for a dynamically typed language).

Links