How to use Enum with SQLAlchemy and Alembic?

Use the following function example in case you are using PostgreSQL:

from sqlalchemy.dialects import postgresql
from ... import PostStatus
from alembic import op
import sqlalchemy as sa

def upgrade():
    post_status = postgresql.ENUM(PostStatus, name="status")
    post_status.create(op.get_bind(), checkfirst=True)
    op.add_column('posts', sa.Column('status',  post_status))

def downgrade():
    post_status = postgresql.ENUM(PostStatus, name="status")

I can only answer the third part of your question.

The documentation for the Enum type in SQLAlchemy states that:

Above, the string names of each element, e.g. “one”, “two”, “three”, are persisted to the database; the values of the Python Enum, here indicated as integers, are not used; the value of each enum can therefore be any kind of Python object whether or not it is persistable.

So, it is by SQLAlchemy design that Enum names, not values are persisted into the database.

Why real values in DB are 'DRAFT', 'APPROVE', 'PUBLISHED', but not draft, etc? I supposed there should be ENUM values, not names.

As Peter Bašista's already mentioned SQLAlchemy uses the enum names (DRAFT, APPROVE, PUBLISHED) in the database. I assume that was done because the enum values ("draft", "approve", ...) can be arbitrary types in Python and they are not guaranteed to be unique (unless @unique is used).

However since SQLAlchemy 1.2.3 the Enum class accepts a parameter values_callable which can be used to store enum values in the database:

    status = db.Column(
        db.Enum(PostStatus, values_callable=lambda obj: [e.value for e in obj]),

Why type poststatus was not created on DB-level automatically? In the similar migration it was.

I think basically you are hitting a limitation of alembic: It won't handle enums on PostgreSQL correctly in some cases. I suspect the main issue in your case is Autogenerate doesn't correctly handle postgresql enums #278.

I noticed that the type is created correctly if I use alembic.op.create_table so my workaround is basically:

enum_type = SQLEnum(PostStatus, values_callable=lambda enum: [e.value for e in enum])
    sa.Column('id', Integer, primary_key=True),
    sa.Column('status', enum_type)
c_status = Column('status', enum_type, nullable=False)
add_column('posts', c_status)