Pagination for entities
function announcements_user($account) {
  $header = array(
    'announcement' =&gt; array(t('Announcement')),
    'operations' =&gt; array('data' =&gt; t('Operations'), 'colspan' =&gt; '2'),
  );
  $rows = array();

  $result = db_select('announcements','a')
    -&gt;fields('a', array('pid','value'))
    -&gt;condition('uid', $account-&gt;uid)
    -&gt;extend('PagerDefault')
    -&gt;limit(5)
    -&gt;execute();

  foreach ($result as $data) {
    $row = array();
    $row[] = l(truncate_utf8($data-&gt;value, 10, FALSE, TRUE), 'announcement/' . $data-&gt;pid);
    $row[] = l(t('Edit'), 'announcement/edit/' . $data-&gt;pid);
    $row[] = l(t('Delete'), 'announcement/delete/' . $data-&gt;pid);

    $rows[] = $row;
  }

  $form['announcements'] = array(
    '#theme' =&gt; 'table', 
    '#header' =&gt; $header, 
    '#rows' =&gt; $rows, 
    '#empty' =&gt; t('No announcement available.'),
  );

  $form['pager'] = array('#markup' =&gt; theme('pager'));
  return $form;
}



Query methods generally return the object for which they are invoked; this means that method calls can be concatenated as I did in  the code.
PagerDefault::limit() has a default limit of 10; if you don't call that method, the limit is the default value.
Strings that appear in the user interface should always be translated; the only strings that cannot be translated (which means, you cannot pass them to t()) are dynamic strings obtained from a function/method.
Drupal URLs should always be obtained from url() or l(). l() returns the HTML tag for links (&lt;a&gt;), while url() returns the relative or absolute URL for the Drupal path passed as argument.


For an example of function that does something similar, see blog_page_last(), which returns the last blog posted by a user.