Mixing Python with Elixir

Generating and Decoding QRCodes using Python modules from Elixir app

The Erlang VM, on which Elixir runs, is efficient for applications in particular domains but falls short when it comes to other operations like data processing and computation-heavy stuff. Python on the other hand, has a rich set of packages for tackling data processing and scientific calculations.

Luckily the Erlang VM allows interoperability with other languages using the Erlang port protocol.

ErlPort is a library for Elixir which makes it easier to connect Erlang to a number of other programming languages using the Erlang port protocol. Currently Erlport supports Python and Ruby.

In this post we’ll see how we can call Python module to generate QRCode right from our Elixir app.

Erlport Python Primer

To use Erlport, just add to your dependencies

    #mix.exs
    {:erlport, "~> 0.9"}

Once Erlport is added to your project, you have access to :python module in your elixir code. This Erlport python module allows you to start an Elixir process to execute python code.

    #creates and Elixir process for calling python functions
    {:ok, pid} = :python.start()

    #get the current python version
    :python.call(pid, :sys, :'version.__str__', [])

Even better, Erlport allows you to configure python path so you can load custom python modules from a specific directory!

    #Automatically load all modules in directory with path /custom/modules/directory

    {:ok, pid} = :python.start([{:python_path, '/custom/modules/directory'}])

With the process returned from calling start/1 , you can call functions in your python module using the familiar MFA - module, function, argument format

    {:ok, pid} = :python.start([{:python_path, 'custom_modules_directory'}])
    module = :test #python module to call
    function = :hello # function in module
    arguments = ["World"] # list of arguments pass to function the function
    result = :python.call(p, module, function, arguments)

If the python function returns data, it will be bound to result variable in the code above. Otherwise :python.call/4 returns :undefined for python functions that don’t return any data.

Now that we are familiar with Erlport let's continue creating our project


Let Create The Elixir Project

In this project, we will create a simple app that encode string to QRCode images. Then decode the QRCode image file to get the string back. Instead of using native Elixir/Erlang lib we will call Python module (qrtools) from our app to do the encoding and decoding. Check here to setup qrtools.

  1. Create the Elixir OTP app
    $ mix new elixir_python_qrcode
  1. Add dependency

Add erlport to your dependencies mix.exs

    defp deps do
        [
          # {:dep_from_hexpm, "~> 0.3.0"},
          # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
          {:erlport, "~> 0.9"}
        ]
      end

Then install project dependencies.

    $ mix deps.get
  1. Lets add wrapper Elixir module for Erlport

Create lib/helper.ex , and add Elixir module called ElixirPythonQrcode.Helper and add these helper functions.

    defmodule ElixirPythonQrcode.Helper do
      @doc """
      ## Parameters
        - path: directory to include in python path (charlist)
      """
      def python_instance(path) when is_list(path) do
        {:ok, pid} = :python.start([{:python_path, to_charlist(path)}])
        pid
      end

      def python_instance(_) do
        {:ok, pid} = :python.start()
        pid
      end

      @doc """
      Call python function using MFA format
      """
      def call_python(pid, module, function, arguments \\ []) do
        pid
        |>:python.call(module, function, arguments)

      end
    end
  1. Add Module code for the main.

We will have two functions encode/2 — which takes in some string and file path. It encodes the data and write the QRCode image to the given file path, and decode/1 — which takes in file path to QRCode image and decodes to get original data.

Let’s edit lib/elixir_python_qrcode.ex

    #lib/elixir_python_qrcode.ex

    defmodule ElixirPythonQrcode do
      @moduledoc """
      Documentation for ElixirPythonQrcode.
      """
      alias ElixirPythonQrcode.Helper

      def encode(data, file_path) do
        call_python(:qrcode, :encode, [data, file_path])
      end

      def decode(file_path) do
        call_python(:qrcode, :decode, [file_path])
      end

      defp default_instance() do
        #Load all modules in our priv/python directory
        path = [:code.priv_dir(:elixir_python_qrcode), "python"]
              |> Path.join()
        Helper.python_instance(to_charlist(path))
      end

      # wrapper function to call python functions using
      # default python instance
      defp call_python(module, function, args \\ []) do
        default_instance()
        |> Helper.call_python(module, function, args)
      end

    end
  1. Now let’s create our python module

Create priv/python/qrcode.py module that will contain our python functions. These functions handle the actual QRCode generation and decoding.

    #priv/python/qrcode.py

    def decode(file_path):
        import qrtools
        qr = qrtools.QR()
        qr.decode(file_path)
        return qr.data

    def encode(data, file_path):
        import qrtools
        qr = qrtools.QR(data.encode("utf-8"))
        return qr.encode(filename=file_path)
  1. Let’s test our code.
    $ iex -S mix
    iex(1)> ElixirPythonQrcode.encode("Some test to encode", "qrcode.png")
    0
    iex(2)> ElixirPythonQrcode.decode("qrcode.png")
    'Some test to encode'
    iex(3)> ElixirPythonQrcode.decode("qrcode.png") |> to_string()
    "Some test to encode"

That’s it. Check out the github repo.

Happy coding!

Check out Part II for Asynchronous communication between Elixir and Python